From 544a5863959e9a1c4d074f30d4a9ddd99de9fa71 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 16 Jun 2024 19:00:59 +0545 Subject: [PATCH 1/7] feat: init v7 rewrite --- apps/music-bot/.eslintrc.json | 7 - apps/music-bot/.gitattributes | 4 - apps/music-bot/.gitignore | 32 - apps/music-bot/.prettierignore | 2 - apps/music-bot/.sapphirerc.json | 15 - apps/music-bot/README.md | 7 - apps/music-bot/package.json | 50 - apps/music-bot/src/.env.example | 3 - apps/music-bot/src/KarasuClient.ts | 70 - apps/music-bot/src/commands/applemusic.ts | 85 - apps/music-bot/src/commands/biquad.ts | 74 - apps/music-bot/src/commands/clear.ts | 37 - apps/music-bot/src/commands/connect.ts | 50 - apps/music-bot/src/commands/disconnect.ts | 32 - apps/music-bot/src/commands/dsp.ts | 61 - apps/music-bot/src/commands/equalizer.ts | 55 - apps/music-bot/src/commands/filters.ts | 71 - apps/music-bot/src/commands/history.ts | 65 - apps/music-bot/src/commands/jump.ts | 72 - apps/music-bot/src/commands/loop.ts | 52 - apps/music-bot/src/commands/lyrics.ts | 68 - apps/music-bot/src/commands/nowplaying.ts | 51 - apps/music-bot/src/commands/pause.ts | 34 - apps/music-bot/src/commands/ping.ts | 55 - apps/music-bot/src/commands/play.ts | 104 - apps/music-bot/src/commands/previous.ts | 39 - apps/music-bot/src/commands/queue.ts | 54 - apps/music-bot/src/commands/record.ts | 81 - apps/music-bot/src/commands/remove.ts | 72 - apps/music-bot/src/commands/shuffle.ts | 37 - apps/music-bot/src/commands/skip.ts | 34 - apps/music-bot/src/commands/skipto.ts | 72 - apps/music-bot/src/commands/soundcloud.ts | 85 - apps/music-bot/src/commands/spotify.ts | 85 - apps/music-bot/src/commands/volume.ts | 40 - apps/music-bot/src/emojis.ts | 4 - apps/music-bot/src/index.ts | 18 - apps/music-bot/src/lib/constants.ts | 6 - apps/music-bot/src/lib/perms.ts | 33 - apps/music-bot/src/lib/setup.ts | 15 - apps/music-bot/src/lib/utils.ts | 44 - .../chatInputCommandDenied.ts | 27 - .../chatInputCommandSuccess.ts | 14 - .../contextMenuCommandDenied.ts | 27 - .../contextMenuCommandSuccess.ts | 14 - .../src/listeners/player/audioTrackAdd.ts | 41 - .../listeners/player/connectionDestroyed.ts | 17 - .../src/listeners/player/connectionError.ts | 15 - apps/music-bot/src/listeners/player/debug.ts | 16 - .../src/listeners/player/disconnect.ts | 22 - .../src/listeners/player/emptyChannel.ts | 22 - .../src/listeners/player/emptyQueue.ts | 20 - apps/music-bot/src/listeners/player/error.ts | 15 - .../src/listeners/player/masterDebug.ts | 16 - .../src/listeners/player/masterError.ts | 15 - .../src/listeners/player/playerError.ts | 22 - .../src/listeners/player/playerStart.ts | 36 - apps/music-bot/src/listeners/ready.ts | 23 - apps/music-bot/src/preconditions/devOnly.ts | 31 - apps/music-bot/templates/command.ts.sapphire | 26 - apps/music-bot/tsconfig.eslint.json | 4 - apps/music-bot/tsconfig.json | 14 - apps/music-bot/tsup.config.ts | 14 - packages/adapter-local/LICENSE | 21 - packages/adapter-local/README.md | 9 - packages/adapter-local/package.json | 32 - packages/adapter-local/src/index.ts | 4 - packages/adapter-local/tsconfig.json | 5 - packages/adapter-local/tsup.config.ts | 6 - packages/adapter-remote/LICENSE | 21 - packages/adapter-remote/README.md | 9 - packages/adapter-remote/package.json | 35 - packages/adapter-remote/src/index.ts | 4 - packages/adapter-remote/tsconfig.json | 5 - packages/adapter-remote/tsup.config.ts | 6 - packages/core/LICENSE | 21 - packages/core/README.md | 11 - packages/core/package.json | 43 - .../core/src/classes/PlayerNodeManager.ts | 143 - packages/core/src/index.ts | 5 - packages/core/src/utils/clients.ts | 4 - packages/core/src/utils/enums.ts | 19 - packages/core/src/worker/AudioNode.ts | 40 - .../core/src/worker/SubscriptionClient.ts | 66 - .../src/worker/actions/CREATE_SUBSCRIPTION.ts | 23 - .../src/worker/actions/DELETE_SUBSCRIPTION.ts | 24 - .../src/worker/actions/GATEWAY_PAYLOAD.ts | 23 - .../src/worker/actions/JOIN_VOICE_CHANNEL.ts | 24 - packages/core/src/worker/actions/PLAY.ts | 25 - .../src/worker/actions/base/BaseAction.ts | 32 - packages/core/src/worker/notifier.ts | 6 - packages/core/src/worker/worker.ts | 30 - packages/core/tsconfig.json | 7 - packages/core/tsup.config.ts | 6 - packages/discord-player/package.json | 10 +- .../src/DefaultVoiceStateHandler.ts | 111 - packages/discord-player/src/Player.ts | 684 +--- .../src/VoiceInterface/StreamDispatcher.ts | 435 --- .../src/VoiceInterface/VoiceUtils.ts | 96 - packages/discord-player/src/adapter.ts | 59 + .../discord-player/src/common/EventEmitter.ts | 97 + packages/discord-player/src/common/types.ts | 2 + packages/discord-player/src/context.ts | 17 + packages/discord-player/src/errors/index.ts | 178 - .../src/extractors/BaseExtractor.ts | 180 - .../extractors/ExtractorExecutionContext.ts | 261 -- .../discord-player/src/fabric/Playlist.ts | 200 -- .../discord-player/src/fabric/SearchResult.ts | 147 - packages/discord-player/src/fabric/Track.ts | 234 -- packages/discord-player/src/fabric/index.ts | 4 - packages/discord-player/src/hooks/common.ts | 63 - .../src/hooks/context/async-context.ts | 89 - packages/discord-player/src/hooks/index.ts | 10 - .../discord-player/src/hooks/stream/index.ts | 2 - .../src/hooks/stream/onAfterCreateStream.ts | 10 - .../src/hooks/stream/onBeforeCreateStream.ts | 10 - .../discord-player/src/hooks/useHistory.ts | 17 - .../discord-player/src/hooks/useMainPlayer.ts | 24 - .../discord-player/src/hooks/useMetadata.ts | 29 - .../discord-player/src/hooks/usePlayer.ts | 16 - packages/discord-player/src/hooks/useQueue.ts | 16 - .../discord-player/src/hooks/useTimeline.ts | 44 - .../discord-player/src/hooks/useVolume.ts | 29 - packages/discord-player/src/index.ts | 45 +- packages/discord-player/src/lrclib/LrcLib.ts | 208 -- .../src/queue/GuildNodeManager.ts | 201 -- .../discord-player/src/queue/GuildQueue.ts | 1164 ------- .../src/queue/GuildQueueAudioFilters.ts | 397 --- .../src/queue/GuildQueueHistory.ts | 122 - .../src/queue/GuildQueuePlayerNode.ts | 733 ----- .../src/queue/GuildQueueStatistics.ts | 54 - .../src/queue/SyncedLyricsProvider.ts | 162 - .../src/queue/VoiceReceiverNode.ts | 101 - packages/discord-player/src/queue/index.ts | 7 - packages/discord-player/src/types/types.ts | 534 --- .../discord-player/src/utils/AsyncQueue.ts | 154 - .../discord-player/src/utils/AudioFilters.ts | 104 - .../discord-player/src/utils/FFmpegStream.ts | 80 - .../discord-player/src/utils/IPRotator.ts | 133 - .../src/utils/PlayerEventsEmitter.ts | 71 - .../discord-player/src/utils/QueryCache.ts | 96 - .../discord-player/src/utils/QueryResolver.ts | 170 - .../src/utils/SequentialBucket.ts | 102 - packages/discord-player/src/utils/TypeUtil.ts | 34 - packages/discord-player/src/utils/Util.ts | 250 -- .../src/utils/__internal__/_container.ts | 5 - .../src/utils/__internal__/addPlayer.ts | 10 - .../src/utils/__internal__/clearPlayer.ts | 6 - .../utils/__internal__/getGlobalRegistry.ts | 5 - .../src/utils/__internal__/getPlayers.ts | 5 - .../src/utils/__internal__/index.ts | 5 - packages/discord-player/src/utils/serde.ts | 57 - .../discord-player/src/voip/connection.ts | 0 packages/discord-player/src/voip/constants.ts | 29 + packages/discord-player/src/voip/libsodium.ts | 80 + .../discord-player/src/voip/networking.ts | 65 + packages/discord-player/src/voip/udp.ts | 134 + packages/discord-player/src/voip/websocket.ts | 117 + packages/downloader/LICENSE | 21 - packages/downloader/README.md | 26 - packages/downloader/package.json | 50 - packages/downloader/src/Downloader.ts | 92 - packages/downloader/src/index.ts | 5 - packages/downloader/tsconfig.json | 6 - packages/downloader/tsup.config.ts | 6 - packages/extractor/LICENSE | 21 - packages/extractor/README.md | 99 - packages/extractor/package.json | 65 - .../src/extractors/AppleMusicExtractor.ts | 267 -- .../src/extractors/AttachmentExtractor.ts | 211 -- .../src/extractors/BridgedExtractor.ts | 24 - .../src/extractors/LyricsExtractor.ts | 56 - .../src/extractors/ReverbnationExtractor.ts | 79 - .../src/extractors/SoundCloudExtractor.ts | 225 -- .../src/extractors/SpotifyExtractor.ts | 378 --- .../src/extractors/VimeoExtractor.ts | 84 - .../src/extractors/YoutubeExtractor.ts | 295 -- .../src/extractors/common/BridgeProvider.ts | 137 - .../extractor/src/extractors/common/helper.ts | 372 --- packages/extractor/src/extractors/index.ts | 11 - packages/extractor/src/index.ts | 5 - packages/extractor/src/internal/AppleMusic.ts | 287 -- packages/extractor/src/internal/Spotify.ts | 294 -- packages/extractor/src/internal/Vimeo.ts | 83 - packages/extractor/src/internal/downloader.ts | 11 - packages/extractor/src/internal/index.ts | 4 - .../extractor/src/types/reverbnation.d.ts | 58 - packages/extractor/src/types/spotify.d.ts | 137 - packages/extractor/tsconfig.json | 6 - packages/extractor/tsup.config.ts | 6 - packages/extractor/typedoc.json | 5 - packages/tsconfig/package.json | 2 +- packages/voice/LICENSE | 21 - packages/voice/README.md | 19 - packages/voice/__test__/sum.spec.ts | 8 - packages/voice/package.json | 48 - packages/voice/src/DiscordVoiceAdapter.ts | 26 - packages/voice/src/VoiceConnection.ts | 26 - packages/voice/src/VoiceManager.ts | 26 - packages/voice/src/common.ts | 1 - packages/voice/src/index.ts | 6 - packages/voice/tsconfig.json | 6 - packages/voice/tsup.config.ts | 6 - packages/voice/vitest.config.ts | 9 - turbo.json | 5 - typedoc.json | 1 - yarn.lock | 2896 +---------------- 207 files changed, 713 insertions(+), 17368 deletions(-) delete mode 100644 apps/music-bot/.eslintrc.json delete mode 100644 apps/music-bot/.gitattributes delete mode 100644 apps/music-bot/.gitignore delete mode 100644 apps/music-bot/.prettierignore delete mode 100644 apps/music-bot/.sapphirerc.json delete mode 100644 apps/music-bot/README.md delete mode 100644 apps/music-bot/package.json delete mode 100644 apps/music-bot/src/.env.example delete mode 100644 apps/music-bot/src/KarasuClient.ts delete mode 100644 apps/music-bot/src/commands/applemusic.ts delete mode 100644 apps/music-bot/src/commands/biquad.ts delete mode 100644 apps/music-bot/src/commands/clear.ts delete mode 100644 apps/music-bot/src/commands/connect.ts delete mode 100644 apps/music-bot/src/commands/disconnect.ts delete mode 100644 apps/music-bot/src/commands/dsp.ts delete mode 100644 apps/music-bot/src/commands/equalizer.ts delete mode 100644 apps/music-bot/src/commands/filters.ts delete mode 100644 apps/music-bot/src/commands/history.ts delete mode 100644 apps/music-bot/src/commands/jump.ts delete mode 100644 apps/music-bot/src/commands/loop.ts delete mode 100644 apps/music-bot/src/commands/lyrics.ts delete mode 100644 apps/music-bot/src/commands/nowplaying.ts delete mode 100644 apps/music-bot/src/commands/pause.ts delete mode 100644 apps/music-bot/src/commands/ping.ts delete mode 100644 apps/music-bot/src/commands/play.ts delete mode 100644 apps/music-bot/src/commands/previous.ts delete mode 100644 apps/music-bot/src/commands/queue.ts delete mode 100644 apps/music-bot/src/commands/record.ts delete mode 100644 apps/music-bot/src/commands/remove.ts delete mode 100644 apps/music-bot/src/commands/shuffle.ts delete mode 100644 apps/music-bot/src/commands/skip.ts delete mode 100644 apps/music-bot/src/commands/skipto.ts delete mode 100644 apps/music-bot/src/commands/soundcloud.ts delete mode 100644 apps/music-bot/src/commands/spotify.ts delete mode 100644 apps/music-bot/src/commands/volume.ts delete mode 100644 apps/music-bot/src/emojis.ts delete mode 100644 apps/music-bot/src/index.ts delete mode 100644 apps/music-bot/src/lib/constants.ts delete mode 100644 apps/music-bot/src/lib/perms.ts delete mode 100644 apps/music-bot/src/lib/setup.ts delete mode 100644 apps/music-bot/src/lib/utils.ts delete mode 100644 apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandDenied.ts delete mode 100644 apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandSuccess.ts delete mode 100644 apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandDenied.ts delete mode 100644 apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandSuccess.ts delete mode 100644 apps/music-bot/src/listeners/player/audioTrackAdd.ts delete mode 100644 apps/music-bot/src/listeners/player/connectionDestroyed.ts delete mode 100644 apps/music-bot/src/listeners/player/connectionError.ts delete mode 100644 apps/music-bot/src/listeners/player/debug.ts delete mode 100644 apps/music-bot/src/listeners/player/disconnect.ts delete mode 100644 apps/music-bot/src/listeners/player/emptyChannel.ts delete mode 100644 apps/music-bot/src/listeners/player/emptyQueue.ts delete mode 100644 apps/music-bot/src/listeners/player/error.ts delete mode 100644 apps/music-bot/src/listeners/player/masterDebug.ts delete mode 100644 apps/music-bot/src/listeners/player/masterError.ts delete mode 100644 apps/music-bot/src/listeners/player/playerError.ts delete mode 100644 apps/music-bot/src/listeners/player/playerStart.ts delete mode 100644 apps/music-bot/src/listeners/ready.ts delete mode 100644 apps/music-bot/src/preconditions/devOnly.ts delete mode 100644 apps/music-bot/templates/command.ts.sapphire delete mode 100644 apps/music-bot/tsconfig.eslint.json delete mode 100644 apps/music-bot/tsconfig.json delete mode 100644 apps/music-bot/tsup.config.ts delete mode 100644 packages/adapter-local/LICENSE delete mode 100644 packages/adapter-local/README.md delete mode 100644 packages/adapter-local/package.json delete mode 100644 packages/adapter-local/src/index.ts delete mode 100644 packages/adapter-local/tsconfig.json delete mode 100644 packages/adapter-local/tsup.config.ts delete mode 100644 packages/adapter-remote/LICENSE delete mode 100644 packages/adapter-remote/README.md delete mode 100644 packages/adapter-remote/package.json delete mode 100644 packages/adapter-remote/src/index.ts delete mode 100644 packages/adapter-remote/tsconfig.json delete mode 100644 packages/adapter-remote/tsup.config.ts delete mode 100644 packages/core/LICENSE delete mode 100644 packages/core/README.md delete mode 100644 packages/core/package.json delete mode 100644 packages/core/src/classes/PlayerNodeManager.ts delete mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/utils/clients.ts delete mode 100644 packages/core/src/utils/enums.ts delete mode 100644 packages/core/src/worker/AudioNode.ts delete mode 100644 packages/core/src/worker/SubscriptionClient.ts delete mode 100644 packages/core/src/worker/actions/CREATE_SUBSCRIPTION.ts delete mode 100644 packages/core/src/worker/actions/DELETE_SUBSCRIPTION.ts delete mode 100644 packages/core/src/worker/actions/GATEWAY_PAYLOAD.ts delete mode 100644 packages/core/src/worker/actions/JOIN_VOICE_CHANNEL.ts delete mode 100644 packages/core/src/worker/actions/PLAY.ts delete mode 100644 packages/core/src/worker/actions/base/BaseAction.ts delete mode 100644 packages/core/src/worker/notifier.ts delete mode 100644 packages/core/src/worker/worker.ts delete mode 100644 packages/core/tsconfig.json delete mode 100644 packages/core/tsup.config.ts delete mode 100644 packages/discord-player/src/DefaultVoiceStateHandler.ts delete mode 100644 packages/discord-player/src/VoiceInterface/StreamDispatcher.ts delete mode 100644 packages/discord-player/src/VoiceInterface/VoiceUtils.ts create mode 100644 packages/discord-player/src/adapter.ts create mode 100644 packages/discord-player/src/common/EventEmitter.ts create mode 100644 packages/discord-player/src/common/types.ts create mode 100644 packages/discord-player/src/context.ts delete mode 100644 packages/discord-player/src/errors/index.ts delete mode 100644 packages/discord-player/src/extractors/BaseExtractor.ts delete mode 100644 packages/discord-player/src/extractors/ExtractorExecutionContext.ts delete mode 100644 packages/discord-player/src/fabric/Playlist.ts delete mode 100644 packages/discord-player/src/fabric/SearchResult.ts delete mode 100644 packages/discord-player/src/fabric/Track.ts delete mode 100644 packages/discord-player/src/fabric/index.ts delete mode 100644 packages/discord-player/src/hooks/common.ts delete mode 100644 packages/discord-player/src/hooks/context/async-context.ts delete mode 100644 packages/discord-player/src/hooks/index.ts delete mode 100644 packages/discord-player/src/hooks/stream/index.ts delete mode 100644 packages/discord-player/src/hooks/stream/onAfterCreateStream.ts delete mode 100644 packages/discord-player/src/hooks/stream/onBeforeCreateStream.ts delete mode 100644 packages/discord-player/src/hooks/useHistory.ts delete mode 100644 packages/discord-player/src/hooks/useMainPlayer.ts delete mode 100644 packages/discord-player/src/hooks/useMetadata.ts delete mode 100644 packages/discord-player/src/hooks/usePlayer.ts delete mode 100644 packages/discord-player/src/hooks/useQueue.ts delete mode 100644 packages/discord-player/src/hooks/useTimeline.ts delete mode 100644 packages/discord-player/src/hooks/useVolume.ts delete mode 100644 packages/discord-player/src/lrclib/LrcLib.ts delete mode 100644 packages/discord-player/src/queue/GuildNodeManager.ts delete mode 100644 packages/discord-player/src/queue/GuildQueue.ts delete mode 100644 packages/discord-player/src/queue/GuildQueueAudioFilters.ts delete mode 100644 packages/discord-player/src/queue/GuildQueueHistory.ts delete mode 100644 packages/discord-player/src/queue/GuildQueuePlayerNode.ts delete mode 100644 packages/discord-player/src/queue/GuildQueueStatistics.ts delete mode 100644 packages/discord-player/src/queue/SyncedLyricsProvider.ts delete mode 100644 packages/discord-player/src/queue/VoiceReceiverNode.ts delete mode 100644 packages/discord-player/src/queue/index.ts delete mode 100644 packages/discord-player/src/types/types.ts delete mode 100644 packages/discord-player/src/utils/AsyncQueue.ts delete mode 100644 packages/discord-player/src/utils/AudioFilters.ts delete mode 100644 packages/discord-player/src/utils/FFmpegStream.ts delete mode 100644 packages/discord-player/src/utils/IPRotator.ts delete mode 100644 packages/discord-player/src/utils/PlayerEventsEmitter.ts delete mode 100644 packages/discord-player/src/utils/QueryCache.ts delete mode 100644 packages/discord-player/src/utils/QueryResolver.ts delete mode 100644 packages/discord-player/src/utils/SequentialBucket.ts delete mode 100644 packages/discord-player/src/utils/TypeUtil.ts delete mode 100644 packages/discord-player/src/utils/Util.ts delete mode 100644 packages/discord-player/src/utils/__internal__/_container.ts delete mode 100644 packages/discord-player/src/utils/__internal__/addPlayer.ts delete mode 100644 packages/discord-player/src/utils/__internal__/clearPlayer.ts delete mode 100644 packages/discord-player/src/utils/__internal__/getGlobalRegistry.ts delete mode 100644 packages/discord-player/src/utils/__internal__/getPlayers.ts delete mode 100644 packages/discord-player/src/utils/__internal__/index.ts delete mode 100644 packages/discord-player/src/utils/serde.ts rename apps/music-bot/src/recordings/.gitkeep => packages/discord-player/src/voip/connection.ts (100%) create mode 100644 packages/discord-player/src/voip/constants.ts create mode 100644 packages/discord-player/src/voip/libsodium.ts create mode 100644 packages/discord-player/src/voip/networking.ts create mode 100644 packages/discord-player/src/voip/udp.ts create mode 100644 packages/discord-player/src/voip/websocket.ts delete mode 100644 packages/downloader/LICENSE delete mode 100644 packages/downloader/README.md delete mode 100644 packages/downloader/package.json delete mode 100644 packages/downloader/src/Downloader.ts delete mode 100644 packages/downloader/src/index.ts delete mode 100644 packages/downloader/tsconfig.json delete mode 100644 packages/downloader/tsup.config.ts delete mode 100644 packages/extractor/LICENSE delete mode 100644 packages/extractor/README.md delete mode 100644 packages/extractor/package.json delete mode 100644 packages/extractor/src/extractors/AppleMusicExtractor.ts delete mode 100644 packages/extractor/src/extractors/AttachmentExtractor.ts delete mode 100644 packages/extractor/src/extractors/BridgedExtractor.ts delete mode 100644 packages/extractor/src/extractors/LyricsExtractor.ts delete mode 100644 packages/extractor/src/extractors/ReverbnationExtractor.ts delete mode 100644 packages/extractor/src/extractors/SoundCloudExtractor.ts delete mode 100644 packages/extractor/src/extractors/SpotifyExtractor.ts delete mode 100644 packages/extractor/src/extractors/VimeoExtractor.ts delete mode 100644 packages/extractor/src/extractors/YoutubeExtractor.ts delete mode 100644 packages/extractor/src/extractors/common/BridgeProvider.ts delete mode 100644 packages/extractor/src/extractors/common/helper.ts delete mode 100644 packages/extractor/src/extractors/index.ts delete mode 100644 packages/extractor/src/index.ts delete mode 100644 packages/extractor/src/internal/AppleMusic.ts delete mode 100644 packages/extractor/src/internal/Spotify.ts delete mode 100644 packages/extractor/src/internal/Vimeo.ts delete mode 100644 packages/extractor/src/internal/downloader.ts delete mode 100644 packages/extractor/src/internal/index.ts delete mode 100644 packages/extractor/src/types/reverbnation.d.ts delete mode 100644 packages/extractor/src/types/spotify.d.ts delete mode 100644 packages/extractor/tsconfig.json delete mode 100644 packages/extractor/tsup.config.ts delete mode 100644 packages/extractor/typedoc.json delete mode 100644 packages/voice/LICENSE delete mode 100644 packages/voice/README.md delete mode 100644 packages/voice/__test__/sum.spec.ts delete mode 100644 packages/voice/package.json delete mode 100644 packages/voice/src/DiscordVoiceAdapter.ts delete mode 100644 packages/voice/src/VoiceConnection.ts delete mode 100644 packages/voice/src/VoiceManager.ts delete mode 100644 packages/voice/src/common.ts delete mode 100644 packages/voice/src/index.ts delete mode 100644 packages/voice/tsconfig.json delete mode 100644 packages/voice/tsup.config.ts delete mode 100644 packages/voice/vitest.config.ts diff --git a/apps/music-bot/.eslintrc.json b/apps/music-bot/.eslintrc.json deleted file mode 100644 index 6c408c9b21..0000000000 --- a/apps/music-bot/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "@sapphire", - "ignorePatterns": ["src/ignore"], - "rules": { - // "no-return-await": "off" - } -} \ No newline at end of file diff --git a/apps/music-bot/.gitattributes b/apps/music-bot/.gitattributes deleted file mode 100644 index af3ad12812..0000000000 --- a/apps/music-bot/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -/.yarn/** linguist-vendored -/.yarn/releases/* binary -/.yarn/plugins/**/* binary -/.pnp.* binary linguist-generated diff --git a/apps/music-bot/.gitignore b/apps/music-bot/.gitignore deleted file mode 100644 index 99d2d23e5c..0000000000 --- a/apps/music-bot/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# Ignore a blackhole and the folder for development -node_modules/ -.vs/ -.idea/ -*.iml - -# Yarn files -.yarn/* -!.yarn/cache -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Other -.DS_Store -dist/ - -# Ignore the config file (contains sensitive information such as tokens) -config.ts - -# Ignore heapsnapshot and log files -*.heapsnapshot -*.log - -# Environment variables -.env -.env.local -.env.development.local -.env.test.local -.env.production.local diff --git a/apps/music-bot/.prettierignore b/apps/music-bot/.prettierignore deleted file mode 100644 index 763301fc00..0000000000 --- a/apps/music-bot/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -dist/ -node_modules/ \ No newline at end of file diff --git a/apps/music-bot/.sapphirerc.json b/apps/music-bot/.sapphirerc.json deleted file mode 100644 index b8d29e8af8..0000000000 --- a/apps/music-bot/.sapphirerc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/sapphiredev/cli/main/templates/schemas/.sapphirerc.scheme.json", - "projectLanguage": "ts", - "locations": { - "base": "src", - "arguments": "arguments", - "commands": "commands", - "listeners": "listeners", - "preconditions": "preconditions" - }, - "customFileTemplates": { - "enabled": true, - "location": "templates" - } -} \ No newline at end of file diff --git a/apps/music-bot/README.md b/apps/music-bot/README.md deleted file mode 100644 index 135ea1aa9e..0000000000 --- a/apps/music-bot/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Music Bot - -## ⚠️ This bot is a fork of [itsauric/auricle-music-bot](https://github.com/itsauric/auricle-music-bot) - -> Commit: [8099da1](https://github.com/itsauric/auricle-music-bot/commit/8099da11ed316785acfaa5d3c326351786fb788c) - -This bot is intended to be used while testing discord-player in development. diff --git a/apps/music-bot/package.json b/apps/music-bot/package.json deleted file mode 100644 index a073cddc8e..0000000000 --- a/apps/music-bot/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "music-bot", - "version": "1.0.0", - "main": "dist/index.js", - "author": "Androz2091 ", - "license": "MIT", - "dependencies": { - "@discord-player/equalizer": "workspace:^", - "@discord-player/extractor": "workspace:^", - "@discord-player/utils": "workspace:^", - "@discordjs/opus": "^0.9.0", - "@distube/ytdl-core": "^4.13.3", - "@sapphire/discord.js-utilities": "^6.0.3", - "@sapphire/duration": "^1.0.0", - "@sapphire/framework": "^4.2.1", - "@sapphire/plugin-api": "^5.0.1", - "@sapphire/plugin-hmr": "^2.0.0", - "@sapphire/plugin-logger": "^3.0.1", - "@skyra/env-utilities": "^1.1.0", - "colorette": "^2.0.19", - "discord-api-types": "^0.37.35", - "discord-player": "workspace:^", - "mediaplex": "^0.0.9", - "opusscript": "^0.0.8", - "play-dl": "^1.9.7", - "youtube-ext": "^1.1.23" - }, - "devDependencies": { - "@sapphire/prettier-config": "^1.4.5", - "@sapphire/ts-config": "^3.3.4", - "@swc/cli": "^0.1.62", - "@swc/core": "^1.3.37", - "@types/node": "^18.14.6", - "@types/ws": "^8.5.4", - "npm-run-all": "^4.1.5", - "prettier": "^2.8.4", - "tsc-watch": "^6.0.0", - "tsup": "^7.2.0", - "tsx": "^3.12.7", - "typescript": "^5.2.2", - "vitest": "^0.34.6" - }, - "scripts": { - "build": "tsup", - "start": "tsup && node dist/index.js", - "dev": "tsx ./src/index.ts", - "format": "prettier --write \"src/**/*.ts\"" - }, - "prettier": "@sapphire/prettier-config" -} diff --git a/apps/music-bot/src/.env.example b/apps/music-bot/src/.env.example deleted file mode 100644 index 907340fbf7..0000000000 --- a/apps/music-bot/src/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -DISCORD_TOKEN= -OWNERS= -YOUTUBE_COOKIE= \ No newline at end of file diff --git a/apps/music-bot/src/KarasuClient.ts b/apps/music-bot/src/KarasuClient.ts deleted file mode 100644 index cd5e8453fa..0000000000 --- a/apps/music-bot/src/KarasuClient.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { BucketScope, LogLevel, SapphireClient } from '@sapphire/framework'; -import { Player } from 'discord-player'; -import { GatewayIntentBits } from 'discord.js'; -import Emojis from './emojis'; -import { envParseArray } from '@skyra/env-utilities'; -import * as Permissions from './lib/perms'; -import path from 'path'; -import { mkdirSync, existsSync } from 'fs'; - -export class KarasuClient extends SapphireClient { - public player: Player; - public dev: typeof Emojis; - public perms: typeof Permissions; - public recordingPath = path.resolve(`${__dirname}/recordings`); - public constructor() { - super({ - disableMentionPrefix: true, - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates], - defaultCooldown: { - filteredUsers: envParseArray('OWNERS'), - scope: BucketScope.User, - delay: 10_000, - limit: 2 - }, - logger: { - level: LogLevel.Debug - } - }); - this.dev = Emojis; - this.perms = Permissions; - - this.player = new Player(this as any, { - ytdlOptions: { - requestOptions: { - headers: { - cookie: process.env.YOUTUBE_COOKIE - } - } - }, - skipFFmpeg: true - }); - - // this.player.events.on('willPlayTrack', (_, __, config, done) => { - // config.dispatcherConfig = { - // ...config.dispatcherConfig, - // disableBiquad: true, - // disableEqualizer: true, - // disableFilters: true, - // disableResampler: true, - // disableVolume: true - // }; - - // done(); - // }); - - if (!existsSync(this.recordingPath)) - mkdirSync(this.recordingPath, { - recursive: true - }); - } -} - -declare module 'discord.js' { - interface Client { - readonly player: Player; - readonly perms: typeof Permissions; - readonly dev: typeof Emojis; - readonly recordingPath: string; - } -} diff --git a/apps/music-bot/src/commands/applemusic.ts b/apps/music-bot/src/commands/applemusic.ts deleted file mode 100644 index 9f873e338d..0000000000 --- a/apps/music-bot/src/commands/applemusic.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { QueryType } from 'discord-player'; -import type { GuildMember } from 'discord.js'; - -export class AppleMusicCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Plays and enqueues track(s) of the query provided from apple music' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => { - return option.setName('query').setDescription('A query of your choice').setRequired(true).setAutocomplete(true); - }); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const query = interaction.options.getString('query'); - const results = await this.container.client.player.search(query!, { - searchEngine: QueryType.APPLE_MUSIC_SEARCH - }); - - return interaction.respond( - results.tracks.slice(0, 10).map((t) => ({ - name: t.title, - value: t.url - })) - ); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const member = interaction.member as GuildMember; - const permissions = this.container.client.perms.voice(interaction, this.container.client); - if (permissions.member()) return interaction.reply({ content: permissions.member(), ephemeral: true }); - if (permissions.client()) return interaction.reply({ content: permissions.client(), ephemeral: true }); - - const query = interaction.options.getString('query'); - - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const results = await this.container.client.player.search(query!, { - searchEngine: QueryType.APPLE_MUSIC_SEARCH - }); - - if (!results.hasTracks()) - return interaction.reply({ - content: `${this.container.client.dev.error} | No tracks were found for your query`, - ephemeral: true - }); - - await interaction.deferReply(); - await interaction.editReply({ content: `⏳ | Loading ${results.playlist ? 'a playlist...' : 'a track...'}` }); - - try { - const res = await this.container.client.player.play(member.voice.channel!.id, results, { - nodeOptions: { - metadata: { - channel: interaction.channel, - client: interaction.guild?.members.me, - requestedBy: interaction.user.username - }, - leaveOnEmptyCooldown: 300000, - leaveOnEmpty: true, - leaveOnEnd: false - } - }); - - await interaction.editReply({ - content: `${this.container.client.dev.success} | Successfully enqueued${ - res.track.playlist ? ` **multiple tracks** from: **${res.track.playlist.title}**` : `: **${res.track.cleanTitle}**` - }` - }); - } catch (error: any) { - await interaction.editReply({ content: `${this.container.client.dev.error} | An error has occurred` }); - return console.log(error); - } - } -} diff --git a/apps/music-bot/src/commands/biquad.ts b/apps/music-bot/src/commands/biquad.ts deleted file mode 100644 index 788f50133c..0000000000 --- a/apps/music-bot/src/commands/biquad.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { BiquadFilterType, useQueue } from 'discord-player'; -import type { APIApplicationCommandOptionChoice } from 'discord.js'; - -type SupportedBiquadFilters = keyof typeof BiquadFilterType | 'Off'; - -export class BiquadCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'The biquad filter that can be applied to tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - const biquadFilters = Object.keys(BiquadFilterType) - .filter((k) => typeof k[0] === 'string') - .map((m) => ({ - name: m, - value: m - })) as APIApplicationCommandOptionChoice[]; - - biquadFilters.unshift({ - name: 'Disable', - value: 'Off' - }); - - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => - option - .setName('filter') - .setDescription('The biquad filter to use') - .addChoices(...biquadFilters) - .setRequired(true) - ) - .addNumberOption((option) => { - return option.setMinValue(-50).setMaxValue(50).setName('gain').setDescription('The dB gain value').setRequired(false); - }); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - const filter = interaction.options.getString('filter', true) as SupportedBiquadFilters; - const dB = interaction.options.getNumber('gain'); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - if (!queue.filters.biquad) - return interaction.reply({ - content: `${this.container.client.dev.error} | The biquad filter is **not available** to be used in this queue`, - ephemeral: true - }); - - if (filter === 'Off') { - queue.filters.biquad.disable(); - } else { - if (typeof dB === 'number') queue.filters.biquad.setGain(dB); - queue.filters.biquad.enable(); - queue.filters.biquad.setFilter(BiquadFilterType[filter]); - } - - return interaction.reply({ - content: `${this.container.client.dev.success} | **Biquad filter** set to: \`${filter}\`` - }); - } -} diff --git a/apps/music-bot/src/commands/clear.ts b/apps/music-bot/src/commands/clear.ts deleted file mode 100644 index dc7ff6b268..0000000000 --- a/apps/music-bot/src/commands/clear.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class ClearCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Clears the current queue and removes all enqueued tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addBooleanOption((option) => option.setName('history').setDescription('Clear the queue history')); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const history = interaction.options.getBoolean('history'); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.tracks) - return interaction.reply({ content: `${this.container.client.dev.error} | There is **nothing** to clear`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - queue.tracks.clear(); - if (history) queue.history.clear(); - return interaction.reply({ - content: `${this.container.client.dev.success} | I have **cleared** the queue` - }); - } -} diff --git a/apps/music-bot/src/commands/connect.ts b/apps/music-bot/src/commands/connect.ts deleted file mode 100644 index d74ff21c90..0000000000 --- a/apps/music-bot/src/commands/connect.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useMainPlayer, useQueue } from 'discord-player'; -import { GuildMember } from 'discord.js'; - -export class DisconnectCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Connects the bot to the voice channel while also creating a new queue' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - if (interaction.member instanceof GuildMember) { - const permissions = this.container.client.perms.voice(interaction, this.container.client); - if (permissions.member()) return interaction.reply({ content: permissions.member(), ephemeral: true }); - if (permissions.client()) return interaction.reply({ content: permissions.client(), ephemeral: true }); - const queue = useQueue(interaction.guild!.id); - const player = useMainPlayer(); - - if (queue) - return interaction.reply({ content: `${this.container.client.dev.error} | I am **already** in a voice channel`, ephemeral: true }); - - const newQueue = player?.queues.create(interaction.guild!.id, { - metadata: { - channel: interaction.channel, - client: interaction.guild?.members.me - }, - leaveOnEmptyCooldown: 300000, - leaveOnEmpty: true, - leaveOnEnd: false, - bufferingTimeout: 0, - volume: 10, - defaultFFmpegFilters: ['lofi', 'bassboost', 'normalizer'] - }); - await newQueue?.connect(interaction.member.voice.channel!.id); - return interaction.reply({ - content: `${this.container.client.dev.success} | I have **successfully connected** to the voice channel` - }); - } - } -} diff --git a/apps/music-bot/src/commands/disconnect.ts b/apps/music-bot/src/commands/disconnect.ts deleted file mode 100644 index 62c7219e39..0000000000 --- a/apps/music-bot/src/commands/disconnect.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class DisconnectCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Disconnects the bot from the voice channel and deletes the queue' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - queue.delete(); - return interaction.reply({ - content: `${this.container.client.dev.success} | I have **successfully disconnected** from the voice channel` - }); - } -} diff --git a/apps/music-bot/src/commands/dsp.ts b/apps/music-bot/src/commands/dsp.ts deleted file mode 100644 index 94e6e116aa..0000000000 --- a/apps/music-bot/src/commands/dsp.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { PCMAudioFilters, PCMFilters, useQueue } from 'discord-player'; - -export class PulsatorCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'The DSP filters that can be applied to tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => - option - .setName('filter') - .setDescription('The filter to toggle') - .addChoices( - ...Object.keys(PCMAudioFilters).map((m) => ({ - name: m, - value: m - })) - ) - .setRequired(true) - ); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - const filter = interaction.options.getString('filter') as PCMFilters; - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - if (!queue.filters.filters) - return interaction.reply({ - content: `${this.container.client.dev.error} | The DSP filters are **not available** to be used in this queue`, - ephemeral: true - }); - - let ff = queue.filters.filters.filters; - if (ff.includes(filter)) { - ff = ff.filter((r) => r !== filter); - } else { - ff.push(filter); - } - - queue.filters.filters.setFilters(ff); - - return interaction.reply({ - content: `${this.container.client.dev.success} | **${filter}** filter has been **${ff.includes(filter) ? 'enabled' : 'disabled'}**` - }); - } -} diff --git a/apps/music-bot/src/commands/equalizer.ts b/apps/music-bot/src/commands/equalizer.ts deleted file mode 100644 index 264a3e87ee..0000000000 --- a/apps/music-bot/src/commands/equalizer.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { EqualizerConfigurationPreset, useQueue } from 'discord-player'; - -export class EqualizerCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'The equalizer filter that can be applied to tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => - option - .setName('preset') - .setDescription('The equalizer filter to use') - .addChoices( - ...Object.keys(EqualizerConfigurationPreset).map((m) => ({ - name: m, - value: m - })) - ) - .setRequired(true) - ); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - const preset = interaction.options.getString('preset') as string; - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - if (!queue.filters.equalizer) - return interaction.reply({ - content: `${this.container.client.dev.error} | The equalizer filter is **not available** to be used in this queue`, - ephemeral: true - }); - - queue.filters.equalizer.setEQ(EqualizerConfigurationPreset[preset]); - queue.filters.equalizer.enable(); - - return interaction.reply({ - content: `${this.container.client.dev.success} | **Equalizer** filter has been set to: **\`${preset}\`**` - }); - } -} diff --git a/apps/music-bot/src/commands/filters.ts b/apps/music-bot/src/commands/filters.ts deleted file mode 100644 index 74810b16d1..0000000000 --- a/apps/music-bot/src/commands/filters.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { FiltersName, useQueue } from 'discord-player'; - -export class FiltersCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'The FFmpeg filters that can be applied to tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => - option - .setName('filter') - .setDescription('The FFmpeg filter to use') - .addChoices( - { name: 'Off', value: 'Off' }, - ...([ - { name: 'lofi', value: 'lofi' }, - { name: '8D', value: '8D' }, - { name: 'bassboost', value: 'bassboost' }, - { name: 'compressor', value: 'compressor' }, - { name: 'karaoke', value: 'karaoke' }, - { name: 'vibrato', value: 'vibrato' }, - { name: 'vaporwave', value: 'vaporwave' }, - { name: 'nightcore', value: 'nightcore' }, - { name: 'tremolo', value: 'tremolo' } - ] as { name: FiltersName; value: FiltersName }[]) - ) - .setRequired(true) - ); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - const filter = interaction.options.getString('filter') as FiltersName | 'Off'; - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - if (!queue.filters.ffmpeg) - return interaction.reply({ - content: `${this.container.client.dev.error} | The FFmpeg filters are **not available** to be used in this queue`, - ephemeral: true - }); - - if (filter === 'Off') { - await queue.filters.ffmpeg.setFilters(false); - return interaction.reply({ - content: `${this.container.client.dev.success} | **Audio** filter has been **disabled**` - }); - } - - await queue.filters.ffmpeg.toggle(filter.includes('bassboost') ? ['bassboost', 'normalizer'] : filter); - - return interaction.reply({ - content: `${this.container.client.dev.success} | **${filter}** filter has been **${ - queue.filters.ffmpeg.isEnabled(filter) ? 'enabled' : 'disabled' - }**` - }); - } -} diff --git a/apps/music-bot/src/commands/history.ts b/apps/music-bot/src/commands/history.ts deleted file mode 100644 index e73f0e4666..0000000000 --- a/apps/music-bot/src/commands/history.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { PaginatedMessage } from '@sapphire/discord.js-utilities'; -import { Command } from '@sapphire/framework'; -import { useHistory, useQueue } from 'discord-player'; - -export class HistoryCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Displays the queue history in an embed' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const history = useHistory(interaction.guild!.id); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!history?.tracks) - return interaction.reply({ - content: `${this.container.client.dev.error} | There is **no** queue history to **display**`, - ephemeral: true - }); - - let pagesNum = Math.ceil(queue.tracks.size / 5); - - if (pagesNum <= 0) { - pagesNum = 1; - } - - const tracks = history.tracks.map((track, idx) => `**${++idx})** [${track.cleanTitle}](${track.url})`); - - const paginatedMessage = new PaginatedMessage(); - - // handle error if pages exceed 25 pages - if (pagesNum > 25) pagesNum = 25; - - for (let i = 0; i < pagesNum; i++) { - const list = tracks.slice(i * 5, i * 5 + 5).join('\n'); - - paginatedMessage.addPageEmbed((embed) => - embed - .setColor('Red') - .setDescription( - `**Queue history** for **session** in **${queue.channel?.name}:**\n${ - list === '' ? '\n*• No more queued tracks*' : `\n${list}` - } - \n` - ) - .setFooter({ - text: `${queue.tracks.size} track(s) in queue` - }) - ); - } - - return paginatedMessage.run(interaction); - } -} diff --git a/apps/music-bot/src/commands/jump.ts b/apps/music-bot/src/commands/jump.ts deleted file mode 100644 index 9a82014c4b..0000000000 --- a/apps/music-bot/src/commands/jump.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class JumpCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Jumps to the given track without removing any previous tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addIntegerOption((option) => - option.setName('track').setDescription('The track you want to jump to').setMinValue(1).setRequired(true).setAutocomplete(true) - ); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const queue = useQueue(interaction.guild!.id); - const track = interaction.options.getInteger('track'); - const jump = queue?.tracks.at(track!); - const position = queue?.node.getTrackPosition(jump!); - - const tracks = queue!.tracks.map((t, idx) => ({ - name: t.title, - value: ++idx - })); - - if (jump?.title && !tracks.some((t) => t.name === jump.title)) { - tracks.unshift({ - name: jump.title, - value: position! - }); - } - - let slicedTracks = tracks.slice(0, 5); - if (track) { - slicedTracks = tracks.slice(track - 1, track + 4); - if (slicedTracks.length > 5) { - slicedTracks = slicedTracks.slice(0, 5); - } - } - - return interaction.respond(slicedTracks); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.tracks) - return interaction.reply({ content: `${this.container.client.dev.error} | There are **no tracks** to **jump** to`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const jump = interaction.options.getInteger('track')! - 1; - const trackResolvable = queue.tracks.at(jump!); - - if (!trackResolvable) - return interaction.reply({ content: `${this.container.client.dev.error} | The **requested track** doesn't **exist**`, ephemeral: true }); - - queue.node.jump(trackResolvable); - return interaction.reply({ - content: `⏩ | I have **jumped** to the track: **${trackResolvable.title}**` - }); - } -} diff --git a/apps/music-bot/src/commands/loop.ts b/apps/music-bot/src/commands/loop.ts deleted file mode 100644 index b808447c2f..0000000000 --- a/apps/music-bot/src/commands/loop.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { QueueRepeatMode, useQueue } from 'discord-player'; - -const repeatModes = [ - { name: 'Off', value: QueueRepeatMode.OFF }, - { name: 'Track', value: QueueRepeatMode.TRACK }, - { name: 'Queue', value: QueueRepeatMode.QUEUE }, - { name: 'Autoplay', value: QueueRepeatMode.AUTOPLAY } -]; - -export class LoopCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Loops the current playing track or the entire queue' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addNumberOption((option) => - option - .setName('mode') - .setDescription('Choose a loop mode') - .setRequired(true) - .addChoices(...repeatModes) - ); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const mode = interaction.options.getNumber('mode', true); - const name = mode === QueueRepeatMode.OFF ? 'Looping' : repeatModes.find((m) => m.value === mode)?.name; - - queue.setRepeatMode(mode as QueueRepeatMode); - - return interaction.reply({ - content: `${this.container.client.dev.success} | **${name}** has been **${mode === queue.repeatMode ? 'enabled' : 'disabled'}**` - }); - } -} diff --git a/apps/music-bot/src/commands/lyrics.ts b/apps/music-bot/src/commands/lyrics.ts deleted file mode 100644 index 6017a9303b..0000000000 --- a/apps/music-bot/src/commands/lyrics.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue, useMainPlayer, Util } from 'discord-player'; -import { EmbedBuilder } from 'discord.js'; - -export class LyricsCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Displays lyrics of the given track' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => { - return option.setName('name').setDescription('The track of the lyrics to search').setRequired(false); - }); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const player = useMainPlayer(); - const queue = useQueue(interaction.guild!.id); - const track = interaction.options.getString('name') || (queue?.currentTrack?.cleanTitle as string); - - await interaction.deferReply(); - - const results = await player.lyrics - .search({ - q: track - }) - .catch((e) => { - console.log(e); - }); - - const lyrics = results?.[0]; - - if (!lyrics?.syncedLyrics && !lyrics?.plainLyrics) - return interaction.editReply({ content: `${this.container.client.dev.error} | No lyrics found for this track` }); - - if (lyrics.syncedLyrics) { - const syncedLyrics = queue?.syncedLyrics(lyrics); - - syncedLyrics?.onChange(async (lyrics, timestamp) => { - await interaction.channel?.send({ - content: `[${Util.formatDuration(timestamp)}]: ${lyrics}` - }); - }); - - syncedLyrics?.subscribe(); - } - - const trimmedLyrics = (lyrics.plainLyrics || lyrics.syncedLyrics || '').substring(0, 1997); - - const embed = new EmbedBuilder() - .setTitle(lyrics.trackName) - .setAuthor({ - name: lyrics.artistName - }) - .setDescription(trimmedLyrics.length === 1997 ? `${trimmedLyrics}...` : trimmedLyrics) - .setColor('Yellow'); - - return interaction.editReply({ embeds: [embed] }); - } -} diff --git a/apps/music-bot/src/commands/nowplaying.ts b/apps/music-bot/src/commands/nowplaying.ts deleted file mode 100644 index f0380ca3c0..0000000000 --- a/apps/music-bot/src/commands/nowplaying.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue, useTimeline } from 'discord-player'; -import { EmbedBuilder } from 'discord.js'; - -export class NowPlayingCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Displays the current track in an embed' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const timeline = useTimeline(interaction.guild!.id)!; - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - - const track = queue.currentTrack; - - const embed = new EmbedBuilder() - .setAuthor({ - name: interaction.user.username, - iconURL: interaction.user.displayAvatarURL() - }) - .setColor('Red') - .setTitle('💿 Now Playing') - .setDescription(`[${track.cleanTitle}](${track.url})`) - .setThumbnail(track.thumbnail ?? interaction.user.displayAvatarURL()) - .addFields([ - { name: 'Author', value: track.author }, - { name: 'Progress', value: `${queue.node.createProgressBar()} (${timeline.timestamp?.progress}%)` }, - { name: 'Extractor', value: `\`${track.extractor?.identifier || 'N/A'}\`` } - ]) - .setFooter({ - text: `Ping: ${queue.ping}ms | Event Loop Lag: ${queue.player.eventLoopLag.toFixed(0)}ms` - }); - - return interaction.reply({ embeds: [embed] }); - } -} diff --git a/apps/music-bot/src/commands/pause.ts b/apps/music-bot/src/commands/pause.ts deleted file mode 100644 index 18b624409a..0000000000 --- a/apps/music-bot/src/commands/pause.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue, useTimeline } from 'discord-player'; - -export class PauseCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Pauses or resumes the current track' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const timeline = useTimeline(interaction.guild!.id)!; - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - timeline.paused ? timeline.resume() : timeline.pause(); - const state = timeline.paused; - return interaction.reply({ content: `${this.container.client.dev.success} | **Playback** has been **${state ? 'paused' : 'resumed'}**` }); - } -} diff --git a/apps/music-bot/src/commands/ping.ts b/apps/music-bot/src/commands/ping.ts deleted file mode 100644 index 3eb498a1d2..0000000000 --- a/apps/music-bot/src/commands/ping.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { isMessageInstance } from '@sapphire/discord.js-utilities'; -import { Command } from '@sapphire/framework'; -import { ApplicationCommandType } from 'discord.js'; - -export class PingCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Returns the round trip and heartbeat' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - registry.registerContextMenuCommand((builder) => { - builder // - .setName(this.name) - .setType(ApplicationCommandType.Message); - }); - registry.registerContextMenuCommand((builder) => { - builder // - .setName(this.name) - .setType(ApplicationCommandType.User); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const msg = await interaction.reply({ content: `Ping?`, fetchReply: true }); - - if (isMessageInstance(msg)) { - const diff = msg.createdTimestamp - interaction.createdTimestamp; - const ping = Math.round(this.container.client.ws.ping); - return interaction.editReply(`The round trip took **${diff}ms** and the heartbeat being **${ping}ms**`); - } - - return interaction.editReply('Failed to retrieve ping...'); - } - - // context menu command - public async contextMenuRun(interaction: Command.ContextMenuCommandInteraction) { - const msg = await interaction.reply({ content: `Ping?`, fetchReply: true }); - - if (isMessageInstance(msg)) { - const diff = msg.createdTimestamp - interaction.createdTimestamp; - const ping = Math.round(this.container.client.ws.ping); - return interaction.editReply(`The round trip took **${diff}ms** and the heartbeat being **${ping}ms**`); - } - - return interaction.editReply('Failed to retrieve ping...'); - } -} diff --git a/apps/music-bot/src/commands/play.ts b/apps/music-bot/src/commands/play.ts deleted file mode 100644 index 62343195e0..0000000000 --- a/apps/music-bot/src/commands/play.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { QueryType, useMainPlayer } from 'discord-player'; -import type { GuildMember } from 'discord.js'; - -export class PlayCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Plays and enqueues track(s) of the query provided' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => { - return option.setName('query').setDescription('A query of your choice').setRequired(true).setAutocomplete(true); - }); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const query = interaction.options.getString('query'); - if (!query) return []; - - const player = useMainPlayer(); - - const results = await player!.search(query!, { - requestedBy: interaction.user, - fallbackSearchEngine: QueryType.YOUTUBE_SEARCH - }); - - let tracks; - tracks = results!.tracks - .map((t) => ({ - name: t.title, - value: t.url - })) - .slice(0, 10); - - if (results.playlist) { - tracks = results!.tracks - .map(() => ({ - name: `${results.playlist!.title} [playlist]`, - value: results.playlist!.url - })) - .slice(0, 1); - } - - return interaction.respond(tracks).catch(() => null); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const player = useMainPlayer(); - const member = interaction.member as GuildMember; - const permissions = this.container.client.perms.voice(interaction, this.container.client); - if (permissions.member()) return interaction.reply({ content: permissions.member(), ephemeral: true }); - if (permissions.client()) return interaction.reply({ content: permissions.client(), ephemeral: true }); - - const query = interaction.options.getString('query'); - - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - await interaction.deferReply(); - const results = await player!.search(query!, { - requestedBy: interaction.user, - fallbackSearchEngine: QueryType.YOUTUBE_SEARCH - }); - - if (!results.hasTracks()) - return interaction.editReply({ - content: `${this.container.client.dev.error} | **No** tracks were found for your query` - }); - - try { - const res = await player!.play(member.voice.channel!.id, results, { - nodeOptions: { - metadata: { - channel: interaction.channel, - client: interaction.guild?.members.me, - requestedBy: interaction.user.username - }, - leaveOnEmptyCooldown: 300000, - leaveOnEmpty: true, - leaveOnEnd: false, - pauseOnEmpty: true, - bufferingTimeout: 0, - volume: 50 - // defaultFFmpegFilters: ['silenceremove'] - } - }); - - return interaction.editReply({ - content: `${this.container.client.dev.success} | Successfully enqueued${ - res.track.playlist ? ` **track(s)** from: **${res.track.playlist.title}**` : `: **${res.track.cleanTitle}**` - }` - }); - } catch (error: any) { - await interaction.editReply({ content: `${this.container.client.dev.error} | An **error** has occurred` }); - return console.log(error); - } - } -} diff --git a/apps/music-bot/src/commands/previous.ts b/apps/music-bot/src/commands/previous.ts deleted file mode 100644 index 536a208dd0..0000000000 --- a/apps/music-bot/src/commands/previous.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useHistory, useQueue } from 'discord-player'; - -export class PreviousCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Plays the previous track' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const history = useHistory(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - if (!history?.previousTrack) - return interaction.reply({ - content: `${this.container.client.dev.error} | There is **no** previous track in the **history**`, - ephemeral: true - }); - - await history.previous(); - return interaction.reply({ - content: `🔁 | I am **replaying** the previous track` - }); - } -} diff --git a/apps/music-bot/src/commands/queue.ts b/apps/music-bot/src/commands/queue.ts deleted file mode 100644 index e4a9975c85..0000000000 --- a/apps/music-bot/src/commands/queue.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { PaginatedMessage } from '@sapphire/discord.js-utilities'; -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class QueueCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Displays the queue in an embed' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.tracks || !queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is **no** queue to **display**`, ephemeral: true }); - - let pagesNum = Math.ceil(queue.tracks.size / 5); - if (pagesNum <= 0) pagesNum = 1; - - const tracks = queue.tracks.map((track, idx) => `**${++idx})** [${track.cleanTitle}](${track.url})`); - const paginatedMessage = new PaginatedMessage(); - - // handle error if pages exceed 25 pages - if (pagesNum > 25) pagesNum = 25; - for (let i = 0; i < pagesNum; i++) { - const list = tracks.slice(i * 5, i * 5 + 5).join('\n'); - - paginatedMessage.addPageEmbed((embed) => - embed - .setColor('Red') - .setDescription( - `**Queue** for **session** in **${queue.channel?.name}:**\n${list === '' ? '\n*• No more queued tracks*' : `\n${list}`} - \n**Now Playing:** [${queue.currentTrack?.title}](${queue.currentTrack?.url})\n` - ) - .setFooter({ - text: `${queue.tracks.size} track(s) in queue` - }) - ); - } - - return paginatedMessage.run(interaction); - } -} diff --git a/apps/music-bot/src/commands/record.ts b/apps/music-bot/src/commands/record.ts deleted file mode 100644 index 2f8bf5aab6..0000000000 --- a/apps/music-bot/src/commands/record.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { EndBehaviorType } from 'discord-voip'; -import { Command } from '@sapphire/framework'; -import type { GuildMember } from 'discord.js'; -import { createWriteStream } from 'fs'; - -export class RecordCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Records and plays back the recording' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addUserOption((option) => { - return option.setName('user').setRequired(false).setDescription('The user to record'); - }); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const member = interaction.member as GuildMember; - const user = interaction.options.getUser('user'); - const target = user ? interaction.guild!.members.resolve(user) : interaction.guild!.members.resolve(member); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (permissions.member(target)) return interaction.reply({ content: permissions.member(target), ephemeral: true }); - if (permissions.client()) return interaction.reply({ content: permissions.client(), ephemeral: true }); - if (permissions.memberToMember(target)) return interaction.reply({ content: permissions.memberToMember(target), ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - await interaction.deferReply(); - - const queue = this.container.client.player.nodes.create(interaction.guildId!, { - // just in case if someone decides to play music - metadata: { - channel: interaction.channel, - client: interaction.guild?.members.me, - requestedBy: interaction.user.username - }, - leaveOnEmptyCooldown: 300000, - leaveOnEmpty: true, - leaveOnEnd: false, - bufferingTimeout: 0 - }); - - try { - await queue.connect(member.voice.channelId!, { deaf: false }); - } catch { - return interaction.followUp('Failed to connect to your channel'); - } - - const stream = queue.voiceReceiver?.recordUser(target!.id, { - mode: 'pcm', - end: EndBehaviorType.AfterSilence - }); - if (!stream) return interaction.followUp('Failed to record that user'); - - stream.once('error', (err) => { - console.error(err); - if (interaction.isRepliable()) interaction.followUp('Something went wrong while recording!'); - queue.delete(); - }); - - const writer = stream.pipe(createWriteStream(`${this.container.client.recordingPath}/recording-${target!.id}.pcm`)); - writer.once('finish', () => { - if (interaction.isRepliable()) interaction.followUp(`Finished writing audio!`); - queue.delete(); - }); - - writer.once('error', (err) => { - console.error(err); - if (interaction.isRepliable()) interaction.followUp('Something went wrong while recording!'); - queue.delete(); - }); - } -} diff --git a/apps/music-bot/src/commands/remove.ts b/apps/music-bot/src/commands/remove.ts deleted file mode 100644 index 283875ad49..0000000000 --- a/apps/music-bot/src/commands/remove.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class removeCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Removes the given track' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addIntegerOption((option) => - option.setName('track').setDescription('The track you want to remove').setMinValue(1).setRequired(true).setAutocomplete(true) - ); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const queue = useQueue(interaction.guild!.id); - const track = interaction.options.getInteger('track'); - const remove = queue?.tracks.at(track!); - const position = queue?.node.getTrackPosition(remove!); - - const tracks = queue!.tracks.map((t, idx) => ({ - name: t.title, - value: ++idx - })); - - if (remove?.title && !tracks.some((t) => t.name === remove.title)) { - tracks.unshift({ - name: remove.title, - value: position! - }); - } - - let slicedTracks = tracks.slice(0, 5); - if (track) { - slicedTracks = tracks.slice(track - 1, track + 4); - if (slicedTracks.length > 5) { - slicedTracks = slicedTracks.slice(0, 5); - } - } - - return interaction.respond(slicedTracks); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.tracks) - return interaction.reply({ content: `${this.container.client.dev.error} | There are **no tracks** to **remove**`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const remove = interaction.options.getInteger('track')! - 1; - const trackResolvable = queue.tracks.at(remove!); - - if (!trackResolvable) - return interaction.reply({ content: `${this.container.client.dev.error} | The **requested track** doesn't **exist**`, ephemeral: true }); - - queue.node.remove(trackResolvable); - return interaction.reply({ - content: `${this.container.client.dev.success} | I have **removed** the track: **${trackResolvable.title}**` - }); - } -} diff --git a/apps/music-bot/src/commands/shuffle.ts b/apps/music-bot/src/commands/shuffle.ts deleted file mode 100644 index d9de5c60fb..0000000000 --- a/apps/music-bot/src/commands/shuffle.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class ShuffleCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Shuffles the tracks in the queue' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - if (queue.tracks.size < 2) - return interaction.reply({ - content: `${this.container.client.dev.error} | There are not **enough tracks** in queue to **shuffle**`, - ephemeral: true - }); - - // queue.tracks.shuffle(); - const status = queue.toggleShuffle(); - return interaction.reply({ content: `${this.container.client.dev.success} | I have **${status ? 'shuffled' : 'unshuffled'}** the queue` }); - } -} diff --git a/apps/music-bot/src/commands/skip.ts b/apps/music-bot/src/commands/skip.ts deleted file mode 100644 index ee8750faec..0000000000 --- a/apps/music-bot/src/commands/skip.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class SkipCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Skips the current track and automatically plays the next' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - queue.node.skip(); - return interaction.reply({ - content: `⏩ | I have **skipped** to the next track` - }); - } -} diff --git a/apps/music-bot/src/commands/skipto.ts b/apps/music-bot/src/commands/skipto.ts deleted file mode 100644 index 7910dfc940..0000000000 --- a/apps/music-bot/src/commands/skipto.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue } from 'discord-player'; - -export class SkipToCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Skips to the given track whilst removing previous tracks' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addIntegerOption((option) => - option.setName('track').setDescription('The track you want to skip to').setMinValue(1).setRequired(true).setAutocomplete(true) - ); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const queue = useQueue(interaction.guild!.id); - const track = interaction.options.getInteger('track'); - const skip = queue?.tracks.at(track!); - const position = queue?.node.getTrackPosition(skip!); - - const tracks = queue!.tracks.map((t, idx) => ({ - name: t.title, - value: ++idx - })); - - if (skip?.title && !tracks.some((t) => t.name === skip.title)) { - tracks.unshift({ - name: skip.title, - value: position! - }); - } - - let slicedTracks = tracks.slice(0, 5); - if (track) { - slicedTracks = tracks.slice(track - 1, track + 4); - if (slicedTracks.length > 5) { - slicedTracks = slicedTracks.slice(0, 5); - } - } - - return interaction.respond(slicedTracks); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const permissions = this.container.client.perms.voice(interaction, this.container.client); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am **not** in a voice channel`, ephemeral: true }); - if (!queue.tracks) - return interaction.reply({ content: `${this.container.client.dev.error} | There are **no tracks** to **skip** to`, ephemeral: true }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const skip = interaction.options.getInteger('track')! - 1; - const trackResolvable = queue.tracks.at(skip!); - - if (!trackResolvable) - return interaction.reply({ content: `${this.container.client.dev.error} | The **requested track** doesn't **exist**`, ephemeral: true }); - - queue.node.skipTo(trackResolvable); - return interaction.reply({ - content: `⏩ | I have **skipped** to the track: **${trackResolvable.title}**` - }); - } -} diff --git a/apps/music-bot/src/commands/soundcloud.ts b/apps/music-bot/src/commands/soundcloud.ts deleted file mode 100644 index 7ba6601496..0000000000 --- a/apps/music-bot/src/commands/soundcloud.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { QueryType } from 'discord-player'; -import type { GuildMember } from 'discord.js'; - -export class SoundcloudCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Plays and enqueues track(s) of the query provided from Soundcloud' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => { - return option.setName('query').setDescription('A query of your choice').setRequired(true).setAutocomplete(true); - }); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const query = interaction.options.getString('query'); - const results = await this.container.client.player.search(query!, { - searchEngine: QueryType.SOUNDCLOUD_SEARCH - }); - - return interaction.respond( - results.tracks.slice(0, 10).map((t) => ({ - name: t.title, - value: t.url - })) - ); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const member = interaction.member as GuildMember; - const permissions = this.container.client.perms.voice(interaction, this.container.client); - if (permissions.member()) return interaction.reply({ content: permissions.member(), ephemeral: true }); - if (permissions.client()) return interaction.reply({ content: permissions.client(), ephemeral: true }); - - const query = interaction.options.getString('query'); - - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const results = await this.container.client.player.search(query!, { - searchEngine: QueryType.SOUNDCLOUD_SEARCH - }); - - if (!results.hasTracks()) - return interaction.reply({ - content: `${this.container.client.dev.error} | No tracks were found for your query`, - ephemeral: true - }); - - await interaction.deferReply(); - await interaction.editReply({ content: `⏳ | Loading ${results.playlist ? 'a playlist...' : 'a track...'}` }); - - try { - const res = await this.container.client.player.play(member.voice.channel!.id, results, { - nodeOptions: { - metadata: { - channel: interaction.channel, - client: interaction.guild?.members.me, - requestedBy: interaction.user.username - }, - leaveOnEmptyCooldown: 300000, - leaveOnEmpty: true, - leaveOnEnd: false - } - }); - - await interaction.editReply({ - content: `${this.container.client.dev.success} | Successfully enqueued${ - res.track.playlist ? ` **multiple tracks** from: **${res.track.playlist.title}**` : `: **${res.track.cleanTitle}**` - }` - }); - } catch (error: any) { - await interaction.editReply({ content: `${this.container.client.dev.error} | An error has occurred` }); - return console.log(error); - } - } -} diff --git a/apps/music-bot/src/commands/spotify.ts b/apps/music-bot/src/commands/spotify.ts deleted file mode 100644 index 5ac229a108..0000000000 --- a/apps/music-bot/src/commands/spotify.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { QueryType } from 'discord-player'; -import type { GuildMember } from 'discord.js'; - -export class SpotifyCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Plays and enqueues track(s) of the query provided from spotify' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addStringOption((option) => { - return option.setName('query').setDescription('A query of your choice').setRequired(true).setAutocomplete(true); - }); - }); - } - - public override async autocompleteRun(interaction: Command.AutocompleteInteraction) { - const query = interaction.options.getString('query'); - const results = await this.container.client.player.search(query!, { - searchEngine: QueryType.SPOTIFY_SEARCH - }); - - return interaction.respond( - results.tracks.slice(0, 10).map((t) => ({ - name: t.title, - value: t.url - })) - ); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const member = interaction.member as GuildMember; - const permissions = this.container.client.perms.voice(interaction, this.container.client); - if (permissions.member()) return interaction.reply({ content: permissions.member(), ephemeral: true }); - if (permissions.client()) return interaction.reply({ content: permissions.client(), ephemeral: true }); - - const query = interaction.options.getString('query'); - - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - const results = await this.container.client.player.search(query!, { - searchEngine: QueryType.SPOTIFY_SEARCH - }); - - if (!results.hasTracks()) - return interaction.reply({ - content: `${this.container.client.dev.error} | No tracks were found for your query`, - ephemeral: true - }); - - await interaction.deferReply(); - await interaction.editReply({ content: `⏳ | Loading ${results.playlist ? 'a playlist...' : 'a track...'}` }); - - try { - const res = await this.container.client.player.play(member.voice.channel!.id, results, { - nodeOptions: { - metadata: { - channel: interaction.channel, - client: interaction.guild?.members.me, - requestedBy: interaction.user.username - }, - leaveOnEmptyCooldown: 300000, - leaveOnEmpty: true, - leaveOnEnd: false - } - }); - - await interaction.editReply({ - content: `${this.container.client.dev.success} | Successfully enqueued${ - res.track.playlist ? ` **multiple tracks** from: **${res.track.playlist.title}**` : `: **${res.track.cleanTitle}**` - }` - }); - } catch (error: any) { - await interaction.editReply({ content: `${this.container.client.dev.error} | An error has occurred` }); - return console.log(error); - } - } -} diff --git a/apps/music-bot/src/commands/volume.ts b/apps/music-bot/src/commands/volume.ts deleted file mode 100644 index 971089afde..0000000000 --- a/apps/music-bot/src/commands/volume.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Command } from '@sapphire/framework'; -import { useQueue, useTimeline } from 'discord-player'; - -export class VolumeCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'Changes the volume of the track and entire queue' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description) - .addIntegerOption((option) => - option.setName('amount').setDescription('The amount of volume you want to change to').setMinValue(0).setMaxValue(100) - ); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - const queue = useQueue(interaction.guild!.id); - const timeline = useTimeline(interaction.guild!.id)!; - const permissions = this.container.client.perms.voice(interaction, this.container.client); - const volume = interaction.options.getInteger('amount'); - - if (!queue) return interaction.reply({ content: `${this.container.client.dev.error} | I am not in a voice channel`, ephemeral: true }); - if (!queue.currentTrack) - return interaction.reply({ content: `${this.container.client.dev.error} | There is no track **currently** playing`, ephemeral: true }); - if (!volume) return interaction.reply({ content: `🔊 | **Current** volume is **${timeline.volume}%**` }); - if (permissions.clientToMember()) return interaction.reply({ content: permissions.clientToMember(), ephemeral: true }); - - timeline.setVolume(volume!); - return interaction.reply({ - content: `${this.container.client.dev.success} | I **changed** the volume to: **${timeline.volume}%**` - }); - } -} diff --git a/apps/music-bot/src/emojis.ts b/apps/music-bot/src/emojis.ts deleted file mode 100644 index bade457f9c..0000000000 --- a/apps/music-bot/src/emojis.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default { - success: '✅', - error: '❌' -}; diff --git a/apps/music-bot/src/index.ts b/apps/music-bot/src/index.ts deleted file mode 100644 index 6fd48eb7b8..0000000000 --- a/apps/music-bot/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { KarasuClient } from './KarasuClient'; -import './lib/setup'; - -const client = new KarasuClient(); - -const main = async () => { - try { - client.logger.info('Logging in...'); - await client.login(); - client.logger.info('Logged in!'); - } catch (error) { - client.logger.fatal(error); - client.destroy(); - process.exit(1); - } -}; - -void main(); diff --git a/apps/music-bot/src/lib/constants.ts b/apps/music-bot/src/lib/constants.ts deleted file mode 100644 index 11bba99300..0000000000 --- a/apps/music-bot/src/lib/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { join } from 'path'; - -export const rootDir = join(__dirname, '..', '..'); -export const srcDir = join(rootDir, 'src'); - -export const RandomLoadingMessage = ['Computing...', 'Thinking...', 'Cooking some food', 'Give me a moment', 'Loading...']; diff --git a/apps/music-bot/src/lib/perms.ts b/apps/music-bot/src/lib/perms.ts deleted file mode 100644 index 9e4c99f16b..0000000000 --- a/apps/music-bot/src/lib/perms.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PermissionsBitField } from 'discord.js'; -import type { KarasuClient } from '../KarasuClient'; - -export function voice(interaction, container: KarasuClient) { - function client() { - const resolved = new PermissionsBitField([ - PermissionsBitField.Flags.Connect, - PermissionsBitField.Flags.Speak, - PermissionsBitField.Flags.ViewChannel - ]); - const missingPerms = interaction.member.voice.channel.permissionsFor(interaction.guild!.members.me!).missing(resolved); - - if (missingPerms.length) - return `${container.dev.error} | I am missing the required voice channel permissions: \`${missingPerms.join(', ')}\``; - } - - function member(target?) { - if (target && !target.member.voice.channel) return `${container.dev.error} | ${target.displayName} is not in a voice channel.`; - if (!interaction.member.voice.channel) return `${container.dev.error} | You need to be in a voice channel.`; - } - - function memberToMember(target) { - if (interaction.member.voice.channelId !== target.member.voice.channelId) - return `${container.dev.error} | You are not in the same voice channel as the **target user**.`; - } - - function clientToMember() { - if (interaction.guild?.members.me?.voice.channelId && interaction.member.voice.channelId !== interaction.guild?.members.me?.voice.channelId) - return `${container.dev.error} | You are not in my voice channel`; - } - - return { client, member, memberToMember, clientToMember }; -} diff --git a/apps/music-bot/src/lib/setup.ts b/apps/music-bot/src/lib/setup.ts deleted file mode 100644 index 2b4db44511..0000000000 --- a/apps/music-bot/src/lib/setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import '@sapphire/plugin-api/register'; -import '@sapphire/plugin-hmr/register'; -import '@sapphire/plugin-logger/register'; -import { setup, type ArrayString } from '@skyra/env-utilities'; -import { join } from 'path'; - -const rootDir = join(__dirname, '..', '..'); -const srcDir = join(rootDir, 'src'); -setup({ path: join(srcDir, '.env') }); - -declare module '@skyra/env-utilities' { - interface Env { - OWNERS: ArrayString; - } -} diff --git a/apps/music-bot/src/lib/utils.ts b/apps/music-bot/src/lib/utils.ts deleted file mode 100644 index f66c93655e..0000000000 --- a/apps/music-bot/src/lib/utils.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ChatInputCommandSuccessPayload, Command, ContextMenuCommandSuccessPayload, MessageCommandSuccessPayload } from '@sapphire/framework'; -import { container } from '@sapphire/framework'; -import { cyan } from 'colorette'; -import type { APIUser } from 'discord-api-types/v9'; -import { Track, TrackResolvable, useQueue } from 'discord-player'; -import type { Guild, User } from 'discord.js'; - -export function logSuccessCommand(payload: ContextMenuCommandSuccessPayload | ChatInputCommandSuccessPayload | MessageCommandSuccessPayload): void { - let successLoggerData: ReturnType; - - if ('interaction' in payload) { - successLoggerData = getSuccessLoggerData(payload.interaction.guild, payload.interaction.user, payload.command); - } else { - successLoggerData = getSuccessLoggerData(payload.message.guild, payload.message.author, payload.command); - } - - container.logger.debug(`${successLoggerData.shard} - ${successLoggerData.commandName} ${successLoggerData.author} ${successLoggerData.sentAt}`); -} - -export function getSuccessLoggerData(guild: Guild | null, user: User, command: Command) { - const shard = getShardInfo(guild?.shardId ?? 0); - const commandName = getCommandInfo(command); - const author = getAuthorInfo(user); - const sentAt = getGuildInfo(guild); - - return { shard, commandName, author, sentAt }; -} - -function getShardInfo(id: number) { - return `[${cyan(id.toString())}]`; -} - -function getCommandInfo(command: Command) { - return cyan(command.name); -} - -function getAuthorInfo(author: User | APIUser) { - return `${author.username}[${cyan(author.id)}]`; -} - -function getGuildInfo(guild: Guild | null) { - if (guild === null) return 'Direct Messages'; - return `${guild.name}[${cyan(guild.id)}]`; -} diff --git a/apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandDenied.ts b/apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandDenied.ts deleted file mode 100644 index f8e4fd6693..0000000000 --- a/apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandDenied.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DurationFormatter } from '@sapphire/duration'; -import type { ChatInputCommandDeniedPayload, Events } from '@sapphire/framework'; -import { Listener, UserError } from '@sapphire/framework'; - -export class UserEvent extends Listener { - public run({ identifier, context, message: content }: UserError, { interaction }: ChatInputCommandDeniedPayload) { - // `context: { silent: true }` should make UserError silent: - // Use cases for this are for example permissions error when running the `eval` command. - if (Reflect.get(Object(context), 'silent')) return; - - if (identifier === 'preconditionCooldown') { - const remaining = Reflect.get(Object(context), 'remaining'); - const ms = new DurationFormatter().format(remaining); - return interaction.reply({ - content: `Slow down! You must wait **${ms}** before using the \`${interaction.commandName}\` comand.`, - allowedMentions: { users: [interaction.user.id], roles: [] }, - ephemeral: true - }); - } - - return interaction.reply({ - content, - allowedMentions: { users: [interaction.user.id], roles: [] }, - ephemeral: true - }); - } -} diff --git a/apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandSuccess.ts b/apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandSuccess.ts deleted file mode 100644 index 84728aa988..0000000000 --- a/apps/music-bot/src/listeners/commands/chatInputCommands/chatInputCommandSuccess.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ChatInputCommandSuccessPayload, Listener, LogLevel } from '@sapphire/framework'; -import type { Logger } from '@sapphire/plugin-logger'; -import { logSuccessCommand } from '../../../lib/utils'; - -export class UserListener extends Listener { - public run(payload: ChatInputCommandSuccessPayload) { - logSuccessCommand(payload); - } - - public onLoad() { - this.enabled = (this.container.logger as Logger).level <= LogLevel.Debug; - return super.onLoad(); - } -} diff --git a/apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandDenied.ts b/apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandDenied.ts deleted file mode 100644 index 64b2d26f0e..0000000000 --- a/apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandDenied.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DurationFormatter } from '@sapphire/duration'; -import type { ContextMenuCommandDeniedPayload, Events } from '@sapphire/framework'; -import { Listener, UserError } from '@sapphire/framework'; - -export class UserEvent extends Listener { - public run({ identifier, context, message: content }: UserError, { interaction }: ContextMenuCommandDeniedPayload) { - // `context: { silent: true }` should make UserError silent: - // Use cases for this are for example permissions error when running the `eval` command. - if (Reflect.get(Object(context), 'silent')) return; - - if (identifier === 'preconditionCooldown') { - const remaining = Reflect.get(Object(context), 'remaining'); - const ms = new DurationFormatter().format(remaining); - return interaction.reply({ - content: `Slow down! You must wait **${ms}** before using the \`${interaction.commandName}\` comand.`, - allowedMentions: { users: [interaction.user.id], roles: [] }, - ephemeral: true - }); - } - - return interaction.reply({ - content, - allowedMentions: { users: [interaction.user.id], roles: [] }, - ephemeral: true - }); - } -} diff --git a/apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandSuccess.ts b/apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandSuccess.ts deleted file mode 100644 index 6b21930fb6..0000000000 --- a/apps/music-bot/src/listeners/commands/contextMenuCommands/contextMenuCommandSuccess.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ContextMenuCommandSuccessPayload, Listener, LogLevel } from '@sapphire/framework'; -import type { Logger } from '@sapphire/plugin-logger'; -import { logSuccessCommand } from '../../../lib/utils'; - -export class UserListener extends Listener { - public run(payload: ContextMenuCommandSuccessPayload) { - logSuccessCommand(payload); - } - - public onLoad() { - this.enabled = (this.container.logger as Logger).level <= LogLevel.Debug; - return super.onLoad(); - } -} diff --git a/apps/music-bot/src/listeners/player/audioTrackAdd.ts b/apps/music-bot/src/listeners/player/audioTrackAdd.ts deleted file mode 100644 index 697cf42c21..0000000000 --- a/apps/music-bot/src/listeners/player/audioTrackAdd.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import type { GuildQueue, Track } from 'discord-player'; -import { Client, GuildTextBasedChannel, PermissionsBitField } from 'discord.js'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'audioTrackAdd' - }); - } - - public run( - queue: GuildQueue<{ - channel: GuildTextBasedChannel; - client: Client; - }>, - track: Track - ) { - const resolved = new PermissionsBitField([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel]); - const missingPerms = queue.metadata.channel.permissionsFor(queue.metadata.client!.user)?.missing(resolved); - if (missingPerms?.length) return; - - return queue.metadata.channel.send({ - embeds: [ - { - title: 'Track Added!', - description: `🎵 | Track **${track.cleanTitle || 'Unknown Title'}** added to the queue!`, - color: 0xffaaaa, - footer: { - text: `Extractor: ${track.extractor?.identifier || 'N/A'}` - }, - thumbnail: { - url: track.thumbnail - } - } - ] - }); - } -} diff --git a/apps/music-bot/src/listeners/player/connectionDestroyed.ts b/apps/music-bot/src/listeners/player/connectionDestroyed.ts deleted file mode 100644 index 7d8cebafef..0000000000 --- a/apps/music-bot/src/listeners/player/connectionDestroyed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import type { GuildQueue } from 'discord-player'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'connectionDestroyed' - }); - } - - public run(queue: GuildQueue) { - console.log(`Voice connection destroyed for ${queue.guild.name}`); - queue.delete(); - } -} diff --git a/apps/music-bot/src/listeners/player/connectionError.ts b/apps/music-bot/src/listeners/player/connectionError.ts deleted file mode 100644 index ed6103aa6e..0000000000 --- a/apps/music-bot/src/listeners/player/connectionError.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'connectionError' - }); - } - - public run(queue, error) { - console.log(`[${queue.guild.name}] Error emitted from the connection: ${error.message}`); - } -} diff --git a/apps/music-bot/src/listeners/player/debug.ts b/apps/music-bot/src/listeners/player/debug.ts deleted file mode 100644 index e6d2b06e34..0000000000 --- a/apps/music-bot/src/listeners/player/debug.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import { cyanBright, gray } from 'colorette'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'debug' - }); - } - - public run(_queue, message) { - console.log(`[${cyanBright('DEBUG')}] ${gray(message)}\n`); - } -} diff --git a/apps/music-bot/src/listeners/player/disconnect.ts b/apps/music-bot/src/listeners/player/disconnect.ts deleted file mode 100644 index d00c929e35..0000000000 --- a/apps/music-bot/src/listeners/player/disconnect.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import { PermissionsBitField } from 'discord.js'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'disconnect' - }); - } - - public run(queue) { - const resolved = new PermissionsBitField([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel]); - const missingPerms = queue.metadata.channel.permissionsFor(queue.metadata.client).missing(resolved); - if (missingPerms.length) return; - - queue.metadata.channel - .send('I have been **manually disconnected** from the **voice channel**') - .then((m: { delete: () => void }) => setTimeout(() => m.delete(), 15000)); - } -} diff --git a/apps/music-bot/src/listeners/player/emptyChannel.ts b/apps/music-bot/src/listeners/player/emptyChannel.ts deleted file mode 100644 index cda467c381..0000000000 --- a/apps/music-bot/src/listeners/player/emptyChannel.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import { PermissionsBitField } from 'discord.js'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'emptyChannel' - }); - } - - public run(queue) { - const resolved = new PermissionsBitField([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel]); - const missingPerms = queue.metadata.channel.permissionsFor(queue.metadata.client).missing(resolved); - if (missingPerms.length) return; - - queue.metadata.channel - .send('I left the channel after **5 minutes** due to **channel inactivity**') - .then((m: { delete: () => void }) => setTimeout(() => m.delete(), 15000)); - } -} diff --git a/apps/music-bot/src/listeners/player/emptyQueue.ts b/apps/music-bot/src/listeners/player/emptyQueue.ts deleted file mode 100644 index 2f1e447494..0000000000 --- a/apps/music-bot/src/listeners/player/emptyQueue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import { PermissionsBitField } from 'discord.js'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'emptyQueue' - }); - } - - public run(queue) { - const resolved = new PermissionsBitField([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel]); - const missingPerms = queue.metadata.channel.permissionsFor(queue.metadata.client).missing(resolved); - if (missingPerms.length) return; - - queue.metadata.channel.send('No more tracks left in the queue!').then((m: { delete: () => void }) => setTimeout(() => m.delete(), 7000)); - } -} diff --git a/apps/music-bot/src/listeners/player/error.ts b/apps/music-bot/src/listeners/player/error.ts deleted file mode 100644 index 9575768ccc..0000000000 --- a/apps/music-bot/src/listeners/player/error.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'error' - }); - } - - public run(queue, error) { - console.log(`[${queue.guild.name}] Error emitted from the queue: ${error.message}`); - } -} diff --git a/apps/music-bot/src/listeners/player/masterDebug.ts b/apps/music-bot/src/listeners/player/masterDebug.ts deleted file mode 100644 index e3a93c662a..0000000000 --- a/apps/music-bot/src/listeners/player/masterDebug.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import { cyanBright, gray } from 'colorette'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player, - event: 'debug' - }); - } - - public run(message) { - console.log(`[${cyanBright('DEBUG')}] ${gray(message)}\n`); - } -} diff --git a/apps/music-bot/src/listeners/player/masterError.ts b/apps/music-bot/src/listeners/player/masterError.ts deleted file mode 100644 index dc25c94219..0000000000 --- a/apps/music-bot/src/listeners/player/masterError.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player, - event: 'error' - }); - } - - public run(error) { - console.log(`Error emitted from player: ${error.message}`); - } -} diff --git a/apps/music-bot/src/listeners/player/playerError.ts b/apps/music-bot/src/listeners/player/playerError.ts deleted file mode 100644 index b4dd574d4a..0000000000 --- a/apps/music-bot/src/listeners/player/playerError.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import { PermissionsBitField } from 'discord.js'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'playerError' - }); - } - - public run(queue, error, track) { - const resolved = new PermissionsBitField([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel]); - const missingPerms = queue.metadata.channel.permissionsFor(queue.metadata.client).missing(resolved); - if (missingPerms.length) return; - - console.log(error); - - return queue.metadata.channel.send(`${queue.metadata.client.dev.error} | There was an error with **${track.cleanTitle}:**`); - } -} diff --git a/apps/music-bot/src/listeners/player/playerStart.ts b/apps/music-bot/src/listeners/player/playerStart.ts deleted file mode 100644 index 22830d1370..0000000000 --- a/apps/music-bot/src/listeners/player/playerStart.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { container, Listener } from '@sapphire/framework'; -import type { Track } from 'discord-player'; -import { PermissionsBitField } from 'discord.js'; - -export class PlayerEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - emitter: container.client.player.events, - event: 'playerStart' - }); - } - - public run(queue, track: Track) { - console.log(track.metadata); - const resolved = new PermissionsBitField([PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel]); - const missingPerms = queue.metadata.channel.permissionsFor(queue.metadata.client).missing(resolved); - if (missingPerms.length) return; - - return queue.metadata.channel.send({ - embeds: [ - { - title: 'Now Playing', - description: `🎵 | **${track.cleanTitle || 'Unknown Title'}**`, - color: 0xaaaaff, - footer: { - text: `Extractor: ${track.extractor?.identifier || 'N/A'}` - }, - thumbnail: { - url: track.thumbnail - } - } - ] - }); - } -} diff --git a/apps/music-bot/src/listeners/ready.ts b/apps/music-bot/src/listeners/ready.ts deleted file mode 100644 index ee27c88fa1..0000000000 --- a/apps/music-bot/src/listeners/ready.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Listener } from '@sapphire/framework'; -import { useMainPlayer } from 'discord-player'; - -export class UserEvent extends Listener { - public constructor(context: Listener.Context, options: Listener.Options) { - super(context, { - ...options, - once: true - }); - } - - public async run() { - console.log(`Logged in as ${this.container.client.user?.username}`); - - const player = useMainPlayer(); - if (player) { - // await player.extractors.loadDefault(/* (ext) => ext !== 'YouTubeExtractor' */); - // console.log(player.scanDeps()); - // await player.extractors.loadDefault((ext) => ext === 'YouTubeExtractor' || ext === 'SpotifyExtractor' || ext === 'AttachmentExtractor'); - await player.extractors.loadDefault(/* (ext) => ext !== 'YouTubeExtractor' */); - } - } -} diff --git a/apps/music-bot/src/preconditions/devOnly.ts b/apps/music-bot/src/preconditions/devOnly.ts deleted file mode 100644 index 5c2bf7de6c..0000000000 --- a/apps/music-bot/src/preconditions/devOnly.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AllFlowsPrecondition } from '@sapphire/framework'; -import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message, Snowflake } from 'discord.js'; -import { envParseArray } from '@skyra/env-utilities'; - -const OWNERS = envParseArray('OWNERS'); - -export class UserPrecondition extends AllFlowsPrecondition { - #message = 'This command can only be used by the owner.'; - - public override chatInputRun(interaction: ChatInputCommandInteraction) { - return this.doOwnerCheck(interaction.user.id); - } - - public override contextMenuRun(interaction: ContextMenuCommandInteraction) { - return this.doOwnerCheck(interaction.user.id); - } - - public override messageRun(message: Message) { - return this.doOwnerCheck(message.author.id); - } - - private doOwnerCheck(userId: Snowflake) { - return OWNERS.includes(userId) ? this.ok() : this.error({ message: this.#message }); - } -} - -declare module '@sapphire/framework' { - interface Preconditions { - devOnly: never; - } -} diff --git a/apps/music-bot/templates/command.ts.sapphire b/apps/music-bot/templates/command.ts.sapphire deleted file mode 100644 index 8c854de2ef..0000000000 --- a/apps/music-bot/templates/command.ts.sapphire +++ /dev/null @@ -1,26 +0,0 @@ -{ - "category": "commands" -} ---- -import { Command } from '@sapphire/framework'; - -export class {{name}}Command extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - description: 'A command' - }); - } - - public override registerApplicationCommands(registry: Command.Registry) { - registry.registerChatInputCommand((builder) => { - builder // - .setName(this.name) - .setDescription(this.description); - }); - } - - public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) { - return interaction.reply({ content: `${interaction.commandName}`, ephemeral: true }); - } -} diff --git a/apps/music-bot/tsconfig.eslint.json b/apps/music-bot/tsconfig.eslint.json deleted file mode 100644 index c2f78f844e..0000000000 --- a/apps/music-bot/tsconfig.eslint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "include": ["src"] -} \ No newline at end of file diff --git a/apps/music-bot/tsconfig.json b/apps/music-bot/tsconfig.json deleted file mode 100644 index daa422f309..0000000000 --- a/apps/music-bot/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "@sapphire/ts-config", - "compilerOptions": { - "experimentalDecorators": true, - "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "dist/.tsbuildinfo", - "skipLibCheck": true, - "noImplicitAny": false, - "noImplicitReturns": false, - "ignoreDeprecations": "5.0" - }, - "include": ["src"] -} diff --git a/apps/music-bot/tsup.config.ts b/apps/music-bot/tsup.config.ts deleted file mode 100644 index 3d316a21ea..0000000000 --- a/apps/music-bot/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - dts: false, - clean: true, - format: ['cjs'], - bundle: false, - skipNodeModulesBundle: true, - keepNames: true, - minify: false, - silent: true, - entry: ['src'], - outDir: 'dist' -}); diff --git a/packages/adapter-local/LICENSE b/packages/adapter-local/LICENSE deleted file mode 100644 index fe07fc7364..0000000000 --- a/packages/adapter-local/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Androz2091 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/adapter-local/README.md b/packages/adapter-local/README.md deleted file mode 100644 index 9a301abd93..0000000000 --- a/packages/adapter-local/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `@discord-player/adapter-local` - -Local PlayerNode adapter that provides interface to Discord Player - -## Installation - -```sh -$ yarn add @discord-player/adapter-local -``` \ No newline at end of file diff --git a/packages/adapter-local/package.json b/packages/adapter-local/package.json deleted file mode 100644 index ec8097e0c3..0000000000 --- a/packages/adapter-local/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@discord-player/adapter-local", - "version": "0.1.0", - "description": "Discord Player local adapter", - "keywords": [ - "discord-player" - ], - "author": "twlite", - "homepage": "https://discord-player.js.org", - "license": "MIT", - "main": "dist/index.js", - "files": [ - "dist" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Androz2091/discord-player.git" - }, - "scripts": { - "build:check": "tsc --noEmit", - "build": "tsup" - }, - "bugs": { - "url": "https://github.com/Androz2091/discord-player/issues" - }, - "devDependencies": { - "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "vitest": "^0.34.6" - } -} diff --git a/packages/adapter-local/src/index.ts b/packages/adapter-local/src/index.ts deleted file mode 100644 index 6b08400873..0000000000 --- a/packages/adapter-local/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export {}; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/adapter-local/tsconfig.json b/packages/adapter-local/tsconfig.json deleted file mode 100644 index 08981710e6..0000000000 --- a/packages/adapter-local/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@discord-player/tsconfig/base.json", - "include": ["src/**/*"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/packages/adapter-local/tsup.config.ts b/packages/adapter-local/tsup.config.ts deleted file mode 100644 index e161ba4d4d..0000000000 --- a/packages/adapter-local/tsup.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '../../tsup.config'; -import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; - -export default defineConfig({ - esbuildPlugins: [esbuildPluginVersionInjector()] -}); diff --git a/packages/adapter-remote/LICENSE b/packages/adapter-remote/LICENSE deleted file mode 100644 index fe07fc7364..0000000000 --- a/packages/adapter-remote/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Androz2091 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/adapter-remote/README.md b/packages/adapter-remote/README.md deleted file mode 100644 index 5661e3ca76..0000000000 --- a/packages/adapter-remote/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# `@discord-player/adapter-remote` - -Remote PlayerNode adapter that provides interface to Discord Player - -## Installation - -```sh -$ yarn add @discord-player/adapter-remote -``` \ No newline at end of file diff --git a/packages/adapter-remote/package.json b/packages/adapter-remote/package.json deleted file mode 100644 index a66b7156e6..0000000000 --- a/packages/adapter-remote/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@discord-player/adapter-remote", - "version": "0.1.0", - "description": "Discord Player remote adapter", - "keywords": [ - "discord-player" - ], - "author": "twlite", - "homepage": "https://discord-player.js.org", - "license": "MIT", - "main": "dist/index.js", - "directories": { - "src": "src" - }, - "files": [ - "dist" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Androz2091/discord-player.git" - }, - "scripts": { - "build:check": "tsc --noEmit", - "build": "tsup" - }, - "bugs": { - "url": "https://github.com/Androz2091/discord-player/issues" - }, - "devDependencies": { - "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "vitest": "^0.34.6" - } -} diff --git a/packages/adapter-remote/src/index.ts b/packages/adapter-remote/src/index.ts deleted file mode 100644 index 6b08400873..0000000000 --- a/packages/adapter-remote/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export {}; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/adapter-remote/tsconfig.json b/packages/adapter-remote/tsconfig.json deleted file mode 100644 index 08981710e6..0000000000 --- a/packages/adapter-remote/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "@discord-player/tsconfig/base.json", - "include": ["src/**/*"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/packages/adapter-remote/tsup.config.ts b/packages/adapter-remote/tsup.config.ts deleted file mode 100644 index e161ba4d4d..0000000000 --- a/packages/adapter-remote/tsup.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '../../tsup.config'; -import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; - -export default defineConfig({ - esbuildPlugins: [esbuildPluginVersionInjector()] -}); diff --git a/packages/core/LICENSE b/packages/core/LICENSE deleted file mode 100644 index fe07fc7364..0000000000 --- a/packages/core/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Androz2091 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/core/README.md b/packages/core/README.md deleted file mode 100644 index 9113d96ad5..0000000000 --- a/packages/core/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `@discord-player/core` - -Discord Player core components - -## Installation - -```sh -$ yarn add @discord-player/core -``` - -This library is internally used by `discord-player`. This library handles all the work related to voice and provides a way to communicate with nodes. \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index b0f242743f..0000000000 --- a/packages/core/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@discord-player/core", - "version": "0.1.0", - "description": "Discord Player core components", - "keywords": [ - "discord-player" - ], - "author": "Androz2091 ", - "homepage": "https://discord-player.js.org", - "license": "MIT", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "directories": { - "dist": "dist", - "src": "src" - }, - "files": [ - "dist" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Androz2091/discord-player.git" - }, - "scripts": { - "build": "tsup", - "build:check": "tsc --noEmit" - }, - "bugs": { - "url": "https://github.com/Androz2091/discord-player/issues" - }, - "dependencies": { - "@discord-player/utils": "workspace:^", - "discord-api-types": "^0.37.2", - "discord-voip": "^0.1.2" - }, - "devDependencies": { - "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "vitest": "^0.34.6" - } -} diff --git a/packages/core/src/classes/PlayerNodeManager.ts b/packages/core/src/classes/PlayerNodeManager.ts deleted file mode 100644 index 620dd819c8..0000000000 --- a/packages/core/src/classes/PlayerNodeManager.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { cpus } from 'node:os'; -import { Worker } from 'node:worker_threads'; -import { join } from 'node:path'; -import { Collection, EventEmitter } from '@discord-player/utils'; -import { WorkerEvents, WorkerOp } from '../utils/enums'; - -interface PlayerNodeConfig { - max?: number | 'auto'; - respawn?: boolean; -} - -interface BasicSubscription { - guild_id: string; - client_id: string; -} - -type WorkerResolvable = number | Worker; - -export interface PlayerNodeEvents { - error: (worker: Worker, error: Error) => Awaited; - message: (worker: Worker, message: unknown) => Awaited; - spawn: (worker: Worker) => Awaited; - debug: (message: string) => Awaited; - voiceStateUpdate: (worker: Worker, payload: any) => Awaited; - subscriptionCreate: (worker: Worker, payload: BasicSubscription) => Awaited; - subscriptionDelete: (worker: Worker, payload: BasicSubscription) => Awaited; -} - -export interface ServicePayload { - op: keyof typeof WorkerOp; - d: { - guild_id: string; - client_id: string; - } & T; -} - -export interface WorkerPayload { - t: keyof typeof WorkerEvents; - d: T; -} - -export class PlayerNodeManager extends EventEmitter { - public workers = new Collection(); - public constructor(public config: PlayerNodeConfig) { - super(); - } - - #debug(message: string) { - this.emit('debug', `[${this.constructor.name} | ${new Date().toLocaleString()}] ${message}`); - } - - public get maxThreads() { - const conf = this.config.max; - if (conf === 'auto') return cpus().length; - if (typeof conf !== 'number' || Number.isNaN(conf) || conf < 1 || !Number.isFinite(conf)) return 1; - return conf; - } - - public get spawnable() { - return this.workers.size < this.maxThreads; - } - - // TODO - public getLeastBusy() { - return; - } - - public send(workerRes: WorkerResolvable, data: ServicePayload) { - const worker = this.resolveWorker(workerRes); - if (!worker) throw new Error('Worker does not exist'); - this.#debug(`Sending ${JSON.stringify(data)} to thread ${worker.threadId}`); - worker.postMessage(data); - } - - public spawn() { - return new Promise((resolve) => { - if (!this.spawnable) return resolve(this.workers.random()!); - - const worker = new Worker(join(__dirname, '..', 'worker', 'worker.js')); - this.#debug(`Spawned worker at thread ${worker.threadId}`); - - worker.on('online', () => { - this.#debug(`worker ${worker.threadId} is online`); - this.workers.set(worker.threadId, worker); - this.emit('spawn', worker); - return resolve(worker); - }); - - worker.on('message', (message: WorkerPayload) => { - this.#debug(`Incoming message from worker ${worker.threadId}\n\n${JSON.stringify(message)}`); - switch (message.t) { - case WorkerEvents.VOICE_STATE_UPDATE: { - return this.emit('voiceStateUpdate', worker, message.d); - } - case WorkerEvents.ERROR: { - return this.emit('error', worker, new Error((message.d as any).message)); - } - case WorkerEvents.SUBSCRIPTION_CREATE: { - return this.emit('subscriptionCreate', worker, message.d as BasicSubscription); - } - case WorkerEvents.SUBSCRIPTION_DELETE: { - return this.emit('subscriptionDelete', worker, message.d as BasicSubscription); - } - default: { - return this.emit('message', worker, message); - } - } - }); - - worker.on('exit', () => { - this.#debug(`Worker terminated at thread ${worker.threadId}`); - this.workers.delete(worker.threadId); - }); - - worker.on('error', (error) => { - this.#debug(`Incoming error message from worker ${worker.threadId}\n\n${JSON.stringify(error)}`); - this.emit('error', worker, error); - }); - }); - } - - public resolveWorker(worker: WorkerResolvable) { - if (typeof worker === 'number') return this.workers.get(worker); - return this.workers.find((res) => res.threadId === worker.threadId); - } - - public async terminate(worker?: WorkerResolvable) { - if (worker) { - const internalWorker = this.resolveWorker(worker); - if (internalWorker) { - this.#debug(`Terminating worker ${internalWorker.threadId}...`); - await internalWorker.terminate(); - this.workers.delete(internalWorker.threadId); - } - } else { - for (const [id, thread] of this.workers) { - this.#debug(`Terminating worker ${thread.threadId}...`); - await thread.terminate(); - this.workers.delete(id); - } - } - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index fb547a9361..0000000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './utils/enums'; -export * from './classes/PlayerNodeManager'; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/core/src/utils/clients.ts b/packages/core/src/utils/clients.ts deleted file mode 100644 index 6f28f3eebb..0000000000 --- a/packages/core/src/utils/clients.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Collection } from '@discord-player/utils'; -import type { SubscriptionClient } from '../worker/SubscriptionClient'; - -export const clients = new Collection(); diff --git a/packages/core/src/utils/enums.ts b/packages/core/src/utils/enums.ts deleted file mode 100644 index 6b54380f86..0000000000 --- a/packages/core/src/utils/enums.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { keyMirror } from '@discord-player/utils'; - -// prettier-ignore -export const WorkerOp = keyMirror([ - "JOIN_VOICE_CHANNEL", - "CREATE_SUBSCRIPTION", - "DELETE_SUBSCRIPTION", - "GATEWAY_PAYLOAD", - "PLAY" -]); - -// prettier-ignore -export const WorkerEvents = keyMirror([ - "SUBSCRIPTION_CREATE", - "SUBSCRIPTION_DELETE", - "VOICE_STATE_UPDATE", - "ERROR", - "CONNECTION_DESTROY" -]); diff --git a/packages/core/src/worker/AudioNode.ts b/packages/core/src/worker/AudioNode.ts deleted file mode 100644 index d0bf05bfae..0000000000 --- a/packages/core/src/worker/AudioNode.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { createAudioPlayer, createAudioResource, StreamType, VoiceConnection } from 'discord-voip'; - -export interface NodePlayerOptions { - query: string; - metadata: unknown; - initialVolume?: number; -} - -export class AudioNode { - public audioPlayer = createAudioPlayer(); - public constructor(public connection: VoiceConnection, public client: string) { - connection.subscribe(this.audioPlayer); - } - - public get guild() { - return this.connection.joinConfig.guildId; - } - - public get channel() { - return this.connection.joinConfig.channelId; - } - - public play(options: NodePlayerOptions) { - const resource = createAudioResource(options.query, { - inputType: StreamType.Arbitrary, - inlineVolume: typeof options.initialVolume === 'number', - metadata: options.metadata - }); - - if ('initialVolume' in options && resource.volume) { - resource.volume.setVolumeLogarithmic(options.initialVolume!); - } - - this.audioPlayer.play(resource); - } - - public destroy() { - this.connection.destroy(); - } -} diff --git a/packages/core/src/worker/SubscriptionClient.ts b/packages/core/src/worker/SubscriptionClient.ts deleted file mode 100644 index febeaf9e00..0000000000 --- a/packages/core/src/worker/SubscriptionClient.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Collection } from '@discord-player/utils'; -import { DiscordGatewayAdapterLibraryMethods, joinVoiceChannel } from 'discord-voip'; -import { WorkerEvents } from '../utils/enums'; -import { AudioNode } from './AudioNode'; -import { notify } from './notifier'; - -export interface SubscriptionPayload { - channelId: string; - guildId: string; - deafen?: boolean; -} - -export class SubscriptionClient { - public subscriptions = new Collection(); - public adapters = new Collection(); - public constructor(public clientId: string) {} - - public connect(config: SubscriptionPayload) { - const voiceConnection = joinVoiceChannel({ - channelId: config.channelId, - guildId: config.guildId, - selfDeaf: Boolean(config.deafen), - adapterCreator: (adapter) => { - this.adapters.set(config.guildId, adapter); - return { - sendPayload: (payload) => { - notify({ - t: WorkerEvents.VOICE_STATE_UPDATE, - d: payload - }); - return true; - }, - destroy: () => { - this.adapters.delete(config.guildId); - this.subscriptions.delete(config.guildId); - notify({ - t: WorkerEvents.CONNECTION_DESTROY, - d: { - client_id: this.clientId, - guild_id: config.guildId, - channel_id: config.channelId - } - }); - } - }; - } - }); - - this.subscriptions.set(voiceConnection.joinConfig.guildId, new AudioNode(voiceConnection, this.clientId)); - } - - public disconnect(config: Pick) { - const node = this.subscriptions.get(config.guildId); - if (node) { - node.connection.destroy(); - this.subscriptions.delete(config.guildId); - } - } - - public disconnectAll() { - for (const [id, node] of this.subscriptions) { - node.connection.destroy(); - this.subscriptions.delete(id); - } - } -} diff --git a/packages/core/src/worker/actions/CREATE_SUBSCRIPTION.ts b/packages/core/src/worker/actions/CREATE_SUBSCRIPTION.ts deleted file mode 100644 index 30d2c75d69..0000000000 --- a/packages/core/src/worker/actions/CREATE_SUBSCRIPTION.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ServicePayload } from '../../classes/PlayerNodeManager'; -import { WorkerEvents, WorkerOp } from '../../utils/enums'; -import { SubscriptionClient } from '../SubscriptionClient'; -import { BaseAction } from './base/BaseAction'; - -class CreateSubscription extends BaseAction { - public actionName = WorkerOp.CREATE_SUBSCRIPTION; - - public handle(data: ServicePayload) { - if (this.isSubscribed(data)) return; - const client = new SubscriptionClient(data.d.client_id); - this.setClient(data, client); - this.notify({ - t: WorkerEvents.SUBSCRIPTION_CREATE, - d: { - client_id: data.d.client_id, - guild_id: data.d.guild_id - } - }); - } -} - -export default new CreateSubscription(); diff --git a/packages/core/src/worker/actions/DELETE_SUBSCRIPTION.ts b/packages/core/src/worker/actions/DELETE_SUBSCRIPTION.ts deleted file mode 100644 index 28aa1fcfa2..0000000000 --- a/packages/core/src/worker/actions/DELETE_SUBSCRIPTION.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ServicePayload } from '../../classes/PlayerNodeManager'; -import { WorkerEvents, WorkerOp } from '../../utils/enums'; -import { BaseAction } from './base/BaseAction'; - -class DeleteSubscription extends BaseAction { - public actionName = WorkerOp.DELETE_SUBSCRIPTION; - - public handle(data: ServicePayload) { - const client = this.getClient(data); - if (client) { - client.disconnectAll(); - this.deleteClient(data); - this.notify({ - t: WorkerEvents.SUBSCRIPTION_DELETE, - d: { - client_id: client.clientId, - guild_id: data.d.guild_id - } - }); - } - } -} - -export default new DeleteSubscription(); diff --git a/packages/core/src/worker/actions/GATEWAY_PAYLOAD.ts b/packages/core/src/worker/actions/GATEWAY_PAYLOAD.ts deleted file mode 100644 index 3b75699b1f..0000000000 --- a/packages/core/src/worker/actions/GATEWAY_PAYLOAD.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ServicePayload } from '../../classes/PlayerNodeManager'; -import { WorkerOp } from '../../utils/enums'; -import { BaseAction } from './base/BaseAction'; -import { GatewayDispatchEvents } from 'discord-api-types/v10'; - -class JoinVoiceChannel extends BaseAction { - public actionName = WorkerOp.GATEWAY_PAYLOAD; - - public async handle(data: ServicePayload) { - const client = this.getClient(data); - if (!client) return; - const adapter = client.adapters.get(data.d.payload.d.guild_id); - if (!adapter) return; - const message = data.d.payload; - if (message.t === GatewayDispatchEvents.VoiceServerUpdate) { - adapter.onVoiceServerUpdate(message.d); - } else if (message.t === GatewayDispatchEvents.VoiceStateUpdate && message.d.session_id && message.d.user_id === client.clientId) { - adapter.onVoiceStateUpdate(message.d); - } - } -} - -export default new JoinVoiceChannel(); diff --git a/packages/core/src/worker/actions/JOIN_VOICE_CHANNEL.ts b/packages/core/src/worker/actions/JOIN_VOICE_CHANNEL.ts deleted file mode 100644 index 0c12a2ba65..0000000000 --- a/packages/core/src/worker/actions/JOIN_VOICE_CHANNEL.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ServicePayload } from '../../classes/PlayerNodeManager'; -import { WorkerOp } from '../../utils/enums'; -import { BaseAction } from './base/BaseAction'; - -export interface JoinPayload { - channel_id: string; - self_deaf?: boolean; -} - -class JoinVoiceChannel extends BaseAction { - public actionName = WorkerOp.JOIN_VOICE_CHANNEL; - - public async handle(data: ServicePayload) { - const client = this.getClient(data); - if (client) - await client.connect({ - channelId: data.d.channel_id, - guildId: data.d.guild_id, - deafen: data.d.self_deaf - }); - } -} - -export default new JoinVoiceChannel(); diff --git a/packages/core/src/worker/actions/PLAY.ts b/packages/core/src/worker/actions/PLAY.ts deleted file mode 100644 index 7760d14836..0000000000 --- a/packages/core/src/worker/actions/PLAY.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ServicePayload } from '../../classes/PlayerNodeManager'; -import { WorkerOp } from '../../utils/enums'; -import { BaseAction } from './base/BaseAction'; - -export interface PlayPayload { - query: string; - metadata: unknown; - initial_volume?: number; -} - -class Play extends BaseAction { - public actionName = WorkerOp.PLAY; - - public async handle(data: ServicePayload) { - const client = this.getClient(data); - if (!client) return; - const node = client.subscriptions.get(data.d.guild_id); - if (node) { - const { query, metadata, initial_volume } = data.d; - node.play({ query, metadata, initialVolume: initial_volume }); - } - } -} - -export default new Play(); diff --git a/packages/core/src/worker/actions/base/BaseAction.ts b/packages/core/src/worker/actions/base/BaseAction.ts deleted file mode 100644 index 447df3a2af..0000000000 --- a/packages/core/src/worker/actions/base/BaseAction.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ServicePayload, WorkerPayload } from '../../../classes/PlayerNodeManager'; -import { clients } from '../../../utils/clients'; -import { WorkerOp } from '../../../utils/enums'; -import { notify } from '../../notifier'; -import { SubscriptionClient } from '../../SubscriptionClient'; - -export class BaseAction { - public clients = clients; - public actionName!: keyof typeof WorkerOp; - - public getClient(data: ServicePayload) { - return this.clients.get(data.d.client_id); - } - - public setClient(data: ServicePayload, client: SubscriptionClient) { - return this.clients.set(data.d.client_id, client); - } - - public deleteClient(data: ServicePayload) { - return this.clients.delete(data.d.client_id); - } - - public isSubscribed(data: ServicePayload) { - return this.clients.has(data.d.client_id); - } - - public handle(data: ServicePayload) {} - - public notify(data: WorkerPayload) { - notify(data); - } -} diff --git a/packages/core/src/worker/notifier.ts b/packages/core/src/worker/notifier.ts deleted file mode 100644 index cfdde3ee3b..0000000000 --- a/packages/core/src/worker/notifier.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { parentPort } from 'node:worker_threads'; -import { WorkerPayload } from '../classes/PlayerNodeManager'; - -export function notify(data: WorkerPayload) { - parentPort?.postMessage(data); -} diff --git a/packages/core/src/worker/worker.ts b/packages/core/src/worker/worker.ts deleted file mode 100644 index 45d18c8b26..0000000000 --- a/packages/core/src/worker/worker.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { ServicePayload } from '../classes/PlayerNodeManager'; -import { WorkerEvents } from '../utils/enums'; -import type { BaseAction } from './actions/base/BaseAction'; -import { notify } from './notifier'; -import { parentPort } from 'node:worker_threads'; - -parentPort?.on('message', async (message: ServicePayload) => { - const action = getAction(message.op); - if (action) { - try { - return void (await action.handle(message)); - } catch (e) { - return notify({ - t: WorkerEvents.ERROR, - d: { - message: `${(e as any).stack || e}` - } - }); - } - } -}); - -function getAction(op: string) { - try { - const action = require(`${__dirname}/actions/${op}`); - return (action.default || action) as BaseAction; - } catch { - return null; - } -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index f19c9a585f..0000000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "@discord-player/tsconfig/base.json", - "include": ["src/**/*"], - "compilerOptions": { - "esModuleInterop": true - } -} \ No newline at end of file diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts deleted file mode 100644 index e161ba4d4d..0000000000 --- a/packages/core/tsup.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '../../tsup.config'; -import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; - -export default defineConfig({ - esbuildPlugins: [esbuildPluginVersionInjector()] -}); diff --git a/packages/discord-player/package.json b/packages/discord-player/package.json index da3fd98fb2..cdb06d2fce 100644 --- a/packages/discord-player/package.json +++ b/packages/discord-player/package.json @@ -1,6 +1,6 @@ { "name": "discord-player", - "version": "6.6.10", + "version": "7.0.0-dev.0", "description": "Complete framework to facilitate music commands using discord.js", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -56,10 +56,8 @@ "@web-scrobbler/metadata-filter": "^3.1.0", "discord-voip": "^0.1.3", "ip": "^2.0.1", - "libsodium-wrappers": "^0.7.13" - }, - "peerDependencies": { - "@discord-player/extractor": "workspace:^" + "libsodium-wrappers": "^0.7.13", + "ws": "^8.17.0" }, "devDependencies": { "@discord-player/tsconfig": "workspace:^", @@ -78,4 +76,4 @@ "readmeFile": "./README.md", "tsconfig": "./tsconfig.json" } -} \ No newline at end of file +} diff --git a/packages/discord-player/src/DefaultVoiceStateHandler.ts b/packages/discord-player/src/DefaultVoiceStateHandler.ts deleted file mode 100644 index 70f97b8e79..0000000000 --- a/packages/discord-player/src/DefaultVoiceStateHandler.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { ChannelType, VoiceState } from 'discord.js'; -import { GuildQueue, GuildQueueEvent } from './queue'; -import { Player } from './Player'; -import { Util } from './utils/Util'; - -export async function defaultVoiceStateHandler(player: Player, queue: GuildQueue, oldState: VoiceState, newState: VoiceState) { - if (!queue || !queue.connection || !queue.channel) return; - - if (oldState.channelId && !newState.channelId && newState.member?.id === newState.guild.members.me?.id) { - try { - queue.delete(); - } catch { - /* noop */ - } - return void player.events.emit(GuildQueueEvent.disconnect, queue); - } - - if (queue.options.pauseOnEmpty) { - const isEmpty = Util.isVoiceEmpty(queue.channel); - - if (isEmpty) { - queue.node.setPaused(true); - Reflect.set(queue, '__pausedOnEmpty', true); - if (queue.hasDebugger) { - queue.debug('Voice channel is empty and options#pauseOnEmpty is true, pausing...'); - } - } else { - if (Reflect.get(queue, '__pausedOnEmpty')) { - queue.node.setPaused(false); - Reflect.set(queue, '__pausedOnEmpty', false); - if (queue.hasDebugger) { - queue.debug('Voice channel is not empty and options#pauseOnEmpty is true, resuming...'); - } - } - } - } - - if (!oldState.channelId && newState.channelId && newState.member?.id === newState.guild.members.me?.id) { - if (newState.serverMute != null && oldState.serverMute !== newState.serverMute) { - queue.node.setPaused(newState.serverMute); - } else if (newState.channel?.type === ChannelType.GuildStageVoice && newState.suppress != null && oldState.suppress !== newState.suppress) { - queue.node.setPaused(newState.suppress); - if (newState.suppress) { - newState.guild.members.me?.voice.setRequestToSpeak(true).catch(Util.noop); - } - } - } - - if (!newState.channelId && oldState.channelId === queue.channel.id) { - if (!Util.isVoiceEmpty(queue.channel)) return; - const timeout = setTimeout(() => { - if (!Util.isVoiceEmpty(queue.channel!)) return; - if (!player.nodes.has(queue.guild.id)) return; - if (queue.options.leaveOnEmpty) queue.delete(); - player.events.emit(GuildQueueEvent.emptyChannel, queue); - }, queue.options.leaveOnEmptyCooldown || 0).unref(); - queue.timeouts.set(`empty_${oldState.guild.id}`, timeout); - } - - if (newState.channelId && newState.channelId === queue.channel.id) { - const emptyTimeout = queue.timeouts.get(`empty_${oldState.guild.id}`); - const channelEmpty = Util.isVoiceEmpty(queue.channel); - if (!channelEmpty && emptyTimeout) { - clearTimeout(emptyTimeout); - queue.timeouts.delete(`empty_${oldState.guild.id}`); - player.events.emit(GuildQueueEvent.channelPopulate, queue); - } - } - - if (oldState.channelId && newState.channelId && oldState.channelId !== newState.channelId) { - if (newState.member?.id === newState.guild.members.me?.id) { - if (queue.connection && newState.member?.id === newState.guild.members.me?.id) queue.channel = newState.channel!; - const emptyTimeout = queue.timeouts.get(`empty_${oldState.guild.id}`); - const channelEmpty = Util.isVoiceEmpty(queue.channel); - if (!channelEmpty && emptyTimeout) { - clearTimeout(emptyTimeout); - queue.timeouts.delete(`empty_${oldState.guild.id}`); - player.events.emit(GuildQueueEvent.channelPopulate, queue); - } else { - const timeout = setTimeout(() => { - if (queue.connection && !Util.isVoiceEmpty(queue.channel!)) return; - if (!player.nodes.has(queue.guild.id)) return; - if (queue.options.leaveOnEmpty) queue.delete(); - player.events.emit(GuildQueueEvent.emptyChannel, queue); - }, queue.options.leaveOnEmptyCooldown || 0).unref(); - queue.timeouts.set(`empty_${oldState.guild.id}`, timeout); - } - } else { - if (newState.channelId !== queue.channel.id) { - const channelEmpty = Util.isVoiceEmpty(queue.channel!); - if (!channelEmpty) return; - if (queue.timeouts.has(`empty_${oldState.guild.id}`)) return; - const timeout = setTimeout(() => { - if (!Util.isVoiceEmpty(queue.channel!)) return; - if (!player.nodes.has(queue.guild.id)) return; - if (queue.options.leaveOnEmpty) queue.delete(); - player.events.emit(GuildQueueEvent.emptyChannel, queue); - }, queue.options.leaveOnEmptyCooldown || 0).unref(); - queue.timeouts.set(`empty_${oldState.guild.id}`, timeout); - } else { - const emptyTimeout = queue.timeouts.get(`empty_${oldState.guild.id}`); - const channelEmpty = Util.isVoiceEmpty(queue.channel!); - if (!channelEmpty && emptyTimeout) { - clearTimeout(emptyTimeout); - queue.timeouts.delete(`empty_${oldState.guild.id}`); - player.events.emit(GuildQueueEvent.channelPopulate, queue); - } - } - } - } -} diff --git a/packages/discord-player/src/Player.ts b/packages/discord-player/src/Player.ts index 446c7dd4e3..2ac871dae1 100644 --- a/packages/discord-player/src/Player.ts +++ b/packages/discord-player/src/Player.ts @@ -1,679 +1,15 @@ -import { FFmpeg } from '@discord-player/ffmpeg'; -import { Client, SnowflakeUtil, VoiceState, IntentsBitField, User, GuildVoiceChannelResolvable, version as djsVersion, Events } from 'discord.js'; -import { Playlist, Track, SearchResult } from './fabric'; -import { GuildQueueEvents, VoiceConnectConfig, GuildNodeCreateOptions, GuildNodeManager, GuildQueue, ResourcePlayOptions, GuildQueueEvent } from './queue'; -import { VoiceUtils } from './VoiceInterface/VoiceUtils'; -import { PlayerEvents, QueryType, SearchOptions, PlayerInitOptions, PlaylistInitData, SearchQueryType, PlayerEvent } from './types/types'; -import { QueryResolver, ResolvedQuery } from './utils/QueryResolver'; -import { Util } from './utils/Util'; -import { generateDependencyReport, version as dVoiceVersion } from 'discord-voip'; -import { ExtractorExecutionContext } from './extractors/ExtractorExecutionContext'; -import { BaseExtractor } from './extractors/BaseExtractor'; -import * as _internals from './utils/__internal__'; -import { QueryCache } from './utils/QueryCache'; -import { PlayerEventsEmitter } from './utils/PlayerEventsEmitter'; -import { Exceptions } from './errors'; -import { defaultVoiceStateHandler } from './DefaultVoiceStateHandler'; -import { IPRotator } from './utils/IPRotator'; -import { Context, createContext } from './hooks'; -import { HooksCtx } from './hooks/common'; -import { LrcLib } from './lrclib/LrcLib'; +import type { IAdapter } from './adapter'; +import { setPlayerAdapterContext } from './context'; -const kSingleton = Symbol('InstanceDiscordPlayerSingleton'); - -export interface PlayerNodeInitializationResult { - track: Track; - extractor: BaseExtractor | null; - searchResult: SearchResult; - queue: GuildQueue; -} - -export type TrackLike = string | Track | SearchResult | Track[] | Playlist; - -export interface PlayerNodeInitializerOptions extends SearchOptions { - nodeOptions?: GuildNodeCreateOptions; - connectionOptions?: VoiceConnectConfig; - audioPlayerOptions?: ResourcePlayOptions; - signal?: AbortSignal; - afterSearch?: (result: SearchResult) => Promise; -} - -export type VoiceStateHandler = (player: Player, queue: GuildQueue, oldVoiceState: VoiceState, newVoiceState: VoiceState) => Awaited; - -export class Player extends PlayerEventsEmitter { - #lastLatency = -1; - #voiceStateUpdateListener = this.handleVoiceState.bind(this); - #lagMonitorTimeout!: NodeJS.Timeout; - #lagMonitorInterval!: NodeJS.Timer; - #onVoiceStateUpdate: VoiceStateHandler = defaultVoiceStateHandler; - #hooksCtx: Context | null = null; - /** - * The version of discord-player - */ - public static readonly version: string = '[VI]{{inject}}[/VI]'; - public static _singletonKey = kSingleton; - /** - * The unique identifier of this player instance - */ - public readonly id = SnowflakeUtil.generate().toString(); - /** - * The discord.js client - */ - public readonly client!: Client; - /** - * The player options - */ - public readonly options!: PlayerInitOptions; - /** - * The player nodes (queue) manager - */ - public nodes = new GuildNodeManager(this); - /** - * The voice api utilities - */ - public readonly voiceUtils = new VoiceUtils(this); - /** - * The extractors manager - */ - public extractors = new ExtractorExecutionContext(this); - /** - * The player events channel - */ - public events = new PlayerEventsEmitter([GuildQueueEvent.Error, GuildQueueEvent.PlayerError]); - /** - * The route planner - */ - public routePlanner: IPRotator | null = null; - /** - * The player version - */ - public readonly version = Player.version; - /** - * The lyrics api - */ - public readonly lyrics = new LrcLib(this); - - /** - * Creates new Discord Player - * @param {Client} client The Discord Client - * @param {PlayerInitOptions} [options] The player init options - */ - public constructor(client: Client, options: PlayerInitOptions = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!options.ignoreInstance && kSingleton in Player) return (Player)[kSingleton] as Player; - - super([PlayerEvent.Error]); - - /** - * The discord.js client - * @type {Client} - */ - this.client = client; - - try { - if (!(client instanceof Client)) { - Util.warn( - `Client is not an instance of discord.js@${djsVersion} client, some things may not work correctly. This can happen due to corrupt dependencies or having multiple installations of discord.js.`, - 'InvalidClientInstance' - ); - } - - const ibf = this.client.options.intents instanceof IntentsBitField ? this.client.options.intents : new IntentsBitField(this.client.options.intents); - - if (!ibf.has(IntentsBitField.Flags.GuildVoiceStates)) { - Util.warn('client is missing "GuildVoiceStates" intent', 'InvalidIntentsBitField'); - } - } catch { - // noop - } - - this.options = { - lockVoiceStateHandler: false, - blockExtractors: [], - blockStreamFrom: [], - connectionTimeout: 20000, - lagMonitor: 30000, - queryCache: options.queryCache === null ? null : options.queryCache || new QueryCache(this), - useLegacyFFmpeg: false, - skipFFmpeg: true, - probeTimeout: 5000, - ...options, - ytdlOptions: { - highWaterMark: 1 << 25, - ...options.ytdlOptions - } - } as PlayerInitOptions; - - this.client.setMaxListeners(this.client.getMaxListeners() + 1); - this.client.on(Events.VoiceStateUpdate, this.#voiceStateUpdateListener); - - if (typeof this.options.lagMonitor === 'number' && this.options.lagMonitor > 0) { - this.#lagMonitorInterval = setInterval(() => { - const start = performance.now(); - this.#lagMonitorTimeout = setTimeout(() => { - this.#lastLatency = performance.now() - start; - if (this.hasDebugger) this.debug(`[Lag Monitor] Event loop latency: ${this.#lastLatency}ms`); - }, 0).unref(); - }, this.options.lagMonitor).unref(); - } - - if (this.options.ipconfig) { - this.routePlanner = new IPRotator(this.options.ipconfig); - } - - _internals.addPlayer(this); - - if (!(kSingleton in Player)) { - Object.defineProperty(Player, kSingleton, { - value: this, - writable: true, - configurable: true, - enumerable: false - }); - } - } - - /** - * The hooks context for this player instance. - */ - public get context() { - if (!this.#hooksCtx) { - this.#hooksCtx = createContext(); - } - - return this.#hooksCtx; - } - - /** - * Override default voice state update handler - * @param handler The handler callback - */ - public onVoiceStateUpdate(handler: VoiceStateHandler) { - this.#onVoiceStateUpdate = handler; - } - - public debug(m: string) { - return this.emit('debug', m); - } - - /** - * Creates discord-player singleton instance. - * @param client The client that instantiated player - * @param options Player initializer options - */ - public static singleton(client: Client, options: Omit = {}) { - return new Player(client, { - ...options, - ignoreInstance: false - }); - } - - /** - * Creates new discord-player instance. - * @param client The client that instantiated player - * @param options Player initializer options - */ - public static create(client: Client, options: Omit = {}) { - return new Player(client, { - ...options, - ignoreInstance: true - }); - } - - /** - * Get all active master player instances - */ - public static getAllPlayers() { - return _internals.getPlayers(); - } - - /** - * Clear all master player instances - */ - public static clearAllPlayers() { - return _internals.instances.clear(); - } - - /** - * The current query cache provider in use - */ - public get queryCache() { - return this.options.queryCache ?? null; - } - - /** - * Alias to `Player.nodes`. - */ - public get queues() { - return this.nodes; - } - - /** - * Event loop latency in ms. If your bot is laggy and this returns a number above 20ms for example, - * some expensive task is being executed on the current thread which is slowing down the event loop. - * @type {number} - */ - public get eventLoopLag() { - return this.#lastLatency; - } - - /** - * Generates statistics that could be useful. Statistics generator is still experimental. - * @example ```typescript - * const stats = player.generateStatistics(); - * - * console.log(stats); - * - * // outputs something like - * // { - * // instances: number, - * // queuesCount: number, - * // queryCacheEnabled: boolean, - * // queues: [ - * // GuildQueueStatisticsMetadata, - * // GuildQueueStatisticsMetadata, - * // GuildQueueStatisticsMetadata, - * // ... - * // ] - * // } - * ``` - */ - public generateStatistics() { - return { - instances: _internals.instances.size, - queuesCount: this.queues.cache.size, - queryCacheEnabled: this.queryCache != null, - queues: this.queues.cache.map((m) => m.stats.generate()) - }; - } - - /** - * Destroy every single queues managed by this master player instance - * @example ```typescript - * // use me when you want to immediately terminate every single queues in existence 🔪 - * await player.destroy(); - * ``` - */ - public async destroy() { - this.nodes.cache.forEach((node) => node.delete()); - this.client.off(Events.VoiceStateUpdate, this.#voiceStateUpdateListener); - this.client.setMaxListeners(this.client.getMaxListeners() - 1); - this.removeAllListeners(); - this.events.removeAllListeners(); - await this.extractors.unregisterAll(); - if (this.#lagMonitorInterval) clearInterval(this.#lagMonitorInterval); - if (this.#lagMonitorTimeout) clearInterval(this.#lagMonitorTimeout); - _internals.clearPlayer(this); - } - - private _handleVoiceState(oldState: VoiceState, newState: VoiceState) { - const queue = this.nodes.get(oldState.guild.id); - if (!queue || !queue.connection || !queue.channel) return; - - // dispatch voice state update - const wasHandled = this.events.emit(GuildQueueEvent.voiceStateUpdate, queue, oldState, newState); - // if the event was handled, return assuming the listener implemented all of the logic below - if (wasHandled && !this.options.lockVoiceStateHandler) return; - - return this.#onVoiceStateUpdate(this, queue, oldState, newState); - } - - /** - * Handles voice state update - * @param {VoiceState} oldState The old voice state - * @param {VoiceState} newState The new voice state - * @returns {void} - * @example ```typescript - * // passing voice state update data to this method will trigger voice state handler - * - * client.on('voiceStateUpdate', (oldState, newState) => { - * // this is definitely a rocket science, right here - * player.handleVoiceState(oldState, newState); - * }); - * ``` - */ - public handleVoiceState(oldState: VoiceState, newState: VoiceState): void { - this._handleVoiceState(oldState, newState); - } - - /** - * Lock voice state handler. When this method is called, discord-player will keep using the default voice state update handler, even if custom implementation exists. - */ - public lockVoiceStateHandler() { - this.options.lockVoiceStateHandler = true; - } - - /** - * Unlock voice state handler. When this method is called, discord-player will stop using the default voice state update handler if custom implementation exists. - */ - public unlockVoiceStateHandler() { - this.options.lockVoiceStateHandler = false; - } - - /** - * Checks if voice state handler is locked. - */ - public isVoiceStateHandlerLocked() { - return !!this.options.lockVoiceStateHandler; - } - - /** - * Initiate audio player - * @param channel The voice channel on which the music should be played - * @param query The track or source to play - * @param options Options for player - * @example ```typescript - * // no need to worry about queue management, just use this method 😄 - * const query = 'this is my super cool search query that I want to play'; - * - * try { - * const { track } = await player.play(voiceChannel, query); - * console.log(`🎉 I am playing ${track.title} 🎉`); - * } catch(e) { - * console.log(`😭 Failed to play error oh no:\n\n${e}`); - * } - * ``` - */ - public async play(channel: GuildVoiceChannelResolvable, query: TrackLike, options: PlayerNodeInitializerOptions = {}): Promise> { - const vc = this.client.channels.resolve(channel); - if (!vc?.isVoiceBased()) throw Exceptions.ERR_INVALID_ARG_TYPE('channel', 'VoiceBasedChannel', !vc ? 'undefined' : `channel type ${vc.type}`); - - const originalResult = query instanceof SearchResult ? query : await this.search(query, options); - const result = (await options.afterSearch?.(originalResult)) || originalResult; - if (result.isEmpty()) { - throw Exceptions.ERR_NO_RESULT(`No results found for "${query}" (Extractor: ${result.extractor?.identifier || 'N/A'})`); - } - - const queue = this.nodes.create(vc.guild, options.nodeOptions); - - if (this.hasDebugger) this.debug(`[AsyncQueue] Acquiring an entry...`); - const entry = queue.tasksQueue.acquire({ signal: options.signal }); - if (this.hasDebugger) this.debug(`[AsyncQueue] Entry ${entry.id} was acquired successfully!`); - - if (this.hasDebugger) this.debug(`[AsyncQueue] Waiting for the queue to resolve...`); - await entry.getTask(); - if (this.hasDebugger) this.debug(`[AsyncQueue] Entry ${entry.id} was resolved!`); - - try { - if (!queue.channel) await queue.connect(vc, options.connectionOptions); - - if (!result.playlist) { - queue.addTrack(result.tracks[0]); - } else { - queue.addTrack(result.playlist); - } - if (!queue.isPlaying()) await queue.node.play(null, options.audioPlayerOptions); - } finally { - if (this.hasDebugger) this.debug(`[AsyncQueue] Releasing an entry from the queue...`); - queue.tasksQueue.release(); - } - - return { - track: result.tracks[0], - extractor: result.extractor, - searchResult: result, - queue - }; - } - - /** - * Search tracks - * @param {string | Track | Track[] | Playlist | SearchResult} query The search query - * @param {SearchOptions} options The search options - * @returns {Promise} - * @example ```typescript - * const searchQuery = 'pass url or text or discord-player track constructable objects, we got you covered 😎'; - * const result = await player.search(searchQuery); - * - * console.log(result); // Logs `SearchResult` object - * ``` - */ - public async search(searchQuery: string | Track | Track[] | Playlist | SearchResult, options: SearchOptions = {}): Promise { - if (searchQuery instanceof SearchResult) return searchQuery; - - if (options.requestedBy != null) options.requestedBy = this.client.users.resolve(options.requestedBy)!; - options.blockExtractors ??= this.options.blockExtractors; - options.fallbackSearchEngine ??= QueryType.AUTO_SEARCH; - - if (searchQuery instanceof Track) { - return new SearchResult(this, { - playlist: searchQuery.playlist || null, - tracks: [searchQuery], - query: searchQuery.title, - extractor: searchQuery.extractor, - queryType: searchQuery.queryType, - requestedBy: options.requestedBy - }); - } - - if (searchQuery instanceof Playlist) { - return new SearchResult(this, { - playlist: searchQuery, - tracks: searchQuery.tracks, - query: searchQuery.title, - extractor: searchQuery.tracks[0]?.extractor, - queryType: QueryType.AUTO, - requestedBy: options.requestedBy - }); - } - - if (Array.isArray(searchQuery)) { - const tracks = searchQuery.filter((t) => t instanceof Track); - return new SearchResult(this, { - playlist: null, - tracks, - query: '@@#%{{UserLoadedContent}}%#@@', - extractor: null, - queryType: QueryType.AUTO, - requestedBy: options.requestedBy - }); - } - - if (this.hasDebugger) this.debug(`Searching ${searchQuery}`); - - let extractor: BaseExtractor | null = null, - protocol: string | null = null; - - options.searchEngine ??= QueryType.AUTO; - options.fallbackSearchEngine ??= QueryType.AUTO_SEARCH; - - if (this.hasDebugger) this.debug(`Search engine set to ${options.searchEngine}, fallback search engine set to ${options.fallbackSearchEngine}`); - - if (/^\w+:/.test(searchQuery)) { - const [protocolName, ...query] = searchQuery.split(':'); - if (this.hasDebugger) this.debug(`Protocol ${protocolName} detected in query`); - - const matchingExtractor = this.extractors.store.find((e) => !this.extractors.isDisabled(e.identifier) && e.protocols.includes(protocolName)); - - if (matchingExtractor) { - if (this.hasDebugger) this.debug(`Protocol ${protocolName} is supported by ${matchingExtractor.identifier} extractor!`); - extractor = matchingExtractor; - searchQuery = query.join(':'); - protocol = protocolName; - } else { - if (this.hasDebugger) this.debug(`Could not find an extractor that supports ${protocolName} protocol. Falling back to default behavior...`); - } - } - - const redirected = await QueryResolver.preResolve(searchQuery); - const { type: queryType, query } = - options.searchEngine === QueryType.AUTO ? QueryResolver.resolve(redirected, options.fallbackSearchEngine) : ({ type: options.searchEngine, query: redirected } as ResolvedQuery); - - if (this.hasDebugger) this.debug(`Query type identified as ${queryType}${extractor && protocol ? ' but might not be used due to the presence of protocol' : ''}`); - - // force particular extractor - if (options.searchEngine.startsWith('ext:')) { - if (this.hasDebugger) this.debug(`Forcing ${options.searchEngine.substring(4)} extractor...`); - extractor = this.extractors.get(options.searchEngine.substring(4))!; - if (!extractor) - return new SearchResult(this, { - query, - queryType, - extractor, - requestedBy: options.requestedBy - }); - } - - // query all extractors - if (!extractor) { - // cache validation - if (!options.ignoreCache) { - if (this.hasDebugger) this.debug(`Checking cache...`); - const res = await this.queryCache?.resolve({ - query, - queryType, - requestedBy: options.requestedBy - }); - // cache hit - if (res?.hasTracks()) { - if (this.hasDebugger) this.debug(`Cache hit for query ${query}`); - return res; - } - - if (this.hasDebugger) this.debug(`Cache miss for query ${query}`); - } - - if (this.hasDebugger) this.debug(`Executing extractors...`); - - // cache miss - extractor = - ( - await this.extractors.run(async (ext) => { - if (options.blockExtractors?.includes(ext.identifier)) return false; - return ext.validate(query, queryType as SearchQueryType); - }) - )?.extractor || null; - } - - // no extractors available - if (!extractor) { - if (this.hasDebugger) this.debug('Failed to find appropriate extractor'); - return new SearchResult(this, { - query, - queryType, - requestedBy: options.requestedBy - }); - } - - if (this.hasDebugger) this.debug(`Executing metadata query using ${extractor.identifier} extractor...`); - const res = await extractor - .handle(query, { - type: queryType as SearchQueryType, - requestedBy: options.requestedBy as User, - requestOptions: options.requestOptions, - protocol - }) - .catch(() => null); - - if (res) { - if (this.hasDebugger) this.debug('Metadata query was successful!'); - const result = new SearchResult(this, { - query, - queryType, - playlist: res.playlist, - tracks: res.tracks, - extractor, - requestedBy: options.requestedBy - }); - - if (!options.ignoreCache) { - if (this.hasDebugger) this.debug(`Adding data to cache...`); - await this.queryCache?.addData(result); - } - - return result; - } - - if (this.hasDebugger) this.debug('Failed to find result using appropriate extractor. Querying all extractors...'); - const result = await this.extractors.run( - async (ext) => - !options.blockExtractors?.includes(ext.identifier) && - (await ext.validate(query)) && - ext.handle(query, { - type: queryType as SearchQueryType, - requestedBy: options.requestedBy as User, - requestOptions: options.requestOptions, - protocol - }) - ); - if (!result?.result) { - if (this.hasDebugger) this.debug(`Failed to query metadata query using ${result?.extractor.identifier || 'N/A'} extractor.`); - return new SearchResult(this, { - query, - queryType, - requestedBy: options.requestedBy, - extractor: result?.extractor - }); - } - - if (this.hasDebugger) this.debug(`Metadata query was successful using ${result.extractor.identifier}!`); - - const data = new SearchResult(this, { - query, - queryType, - playlist: result.result.playlist, - tracks: result.result.tracks, - extractor: result.extractor, - requestedBy: options.requestedBy - }); - - if (!options.ignoreCache) { - if (this.hasDebugger) this.debug(`Adding data to cache...`); - await this.queryCache?.addData(data); - } - - return data; - } - - /** - * Generates a report of the dependencies used by the `discord-voip` module. Useful for debugging. - * @example ```typescript - * console.log(player.scanDeps()); - * // -> logs dependencies report - * ``` - * @returns {string} - */ - public scanDeps() { - const line = '-'.repeat(50); - const runtime = 'Bun' in globalThis ? 'Bun' : 'Deno' in globalThis ? 'Deno' : 'Node'; - const depsReport = [ - 'Discord Player', - line, - `- discord-player: ${Player.version}`, - `- discord-voip: ${dVoiceVersion}`, - `- discord.js: ${djsVersion}`, - `- Node version: ${process.version} (Detected Runtime: ${runtime}, Platform: ${process.platform} [${process.arch}])`, - (() => { - if (this.options.useLegacyFFmpeg) return '- ffmpeg: N/A (using legacy ffmpeg)'; - const info = FFmpeg.locateSafe(); - if (!info) return 'FFmpeg/Avconv not found'; - - return [`- ffmpeg: ${info.version}`, `- command: ${info.command}`, `- static: ${info.isStatic}`, `- libopus: ${info.metadata!.includes('--enable-libopus')}`].join('\n'); - })(), - '\n', - 'Loaded Extractors:', - line, - this.extractors.store - .map((m) => { - return m.identifier; - }) - .join('\n') || 'N/A', - '\n\ndiscord-voip', - generateDependencyReport() - ]; - - return depsReport.join('\n'); +export class Player { + public readonly adapter: IAdapter; + public constructor(adapter: PlayerAdapterInterface) { + this.adapter = setPlayerAdapterContext(this, adapter); } +} - public *[Symbol.iterator]() { - yield* this.nodes.cache.values(); - } +export type PlayerAdapterInterface = () => IAdapter; - /** - * Creates `Playlist` instance - * @param data The data to initialize a playlist - */ - public createPlaylist(data: PlaylistInitData) { - return new Playlist(this, data); - } +export function createPlayer(adapter: PlayerAdapterInterface>) { + return new Player(adapter); } diff --git a/packages/discord-player/src/VoiceInterface/StreamDispatcher.ts b/packages/discord-player/src/VoiceInterface/StreamDispatcher.ts deleted file mode 100644 index 91a569e095..0000000000 --- a/packages/discord-player/src/VoiceInterface/StreamDispatcher.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { - AudioPlayer, - AudioPlayerError, - AudioPlayerStatus, - AudioResource, - createAudioPlayer, - createAudioResource, - entersState, - StreamType, - VoiceConnection, - VoiceConnectionStatus, - VoiceConnectionDisconnectReason -} from 'discord-voip'; -import { StageChannel, VoiceChannel } from 'discord.js'; -import type { Readable } from 'stream'; -import { EventEmitter } from '@discord-player/utils'; -import { Track } from '../fabric/Track'; -import { Util } from '../utils/Util'; -import { EqualizerBand, BiquadFilters, PCMFilters, FiltersChain } from '@discord-player/equalizer'; -import { GuildQueue, GuildQueueEvent, PostProcessedResult } from '../queue'; -import { VoiceReceiverNode } from '../queue/VoiceReceiverNode'; -import { Exceptions } from '../errors'; - -export interface CreateStreamOps { - type?: StreamType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: any; - disableVolume?: boolean; - disableEqualizer?: boolean; - disableBiquad?: boolean; - eq?: EqualizerBand[]; - biquadFilter?: BiquadFilters; - disableFilters?: boolean; - defaultFilters?: PCMFilters[]; - volume?: number; - disableResampler?: boolean; - sampleRate?: number; - skipFFmpeg?: boolean; -} - -export interface VoiceEvents { - /* eslint-disable @typescript-eslint/no-explicit-any */ - error: (error: AudioPlayerError) => any; - debug: (message: string) => any; - start: (resource: AudioResource) => any; - finish: (resource: AudioResource) => any; - dsp: (filters: PCMFilters[]) => any; - eqBands: (filters: EqualizerBand[]) => any; - sampleRate: (filters: number) => any; - biquad: (filters: BiquadFilters) => any; - volume: (volume: number) => any; - destroyed: () => any; - /* eslint-enable @typescript-eslint/no-explicit-any */ -} - -class StreamDispatcher extends EventEmitter { - public voiceConnection: VoiceConnection; - public audioPlayer: AudioPlayer; - public receiver = new VoiceReceiverNode(this); - public channel: VoiceChannel | StageChannel; - public audioResource?: AudioResource | null; - public dsp = new FiltersChain(); - - /** - * Creates new connection object - * @param {VoiceConnection} connection The connection - * @param {VoiceChannel|StageChannel} channel The connected channel - * @private - */ - constructor(connection: VoiceConnection, channel: VoiceChannel | StageChannel, public queue: GuildQueue, public readonly connectionTimeout: number = 20000, audioPlayer?: AudioPlayer) { - super(); - - /** - * The voice connection - * @type {VoiceConnection} - */ - this.voiceConnection = connection; - - /** - * The audio player - * @type {AudioPlayer} - */ - this.audioPlayer = - audioPlayer || - createAudioPlayer({ - debug: this.queue.hasDebugger - }); - - /** - * The voice channel - * @type {VoiceChannel|StageChannel} - */ - this.channel = channel; - - this.voiceConnection.on('debug', (m) => void this.emit('debug', m)); - this.voiceConnection.on('error', (error) => void this.emit('error', error as AudioPlayerError)); - this.audioPlayer.on('debug', (m) => void this.emit('debug', m)); - this.audioPlayer.on('error', (error) => void this.emit('error', error)); - - this.dsp.onUpdate = () => { - if (!this.dsp) return; - if (this.dsp.filters?.filters) this.emit('dsp', this.dsp.filters?.filters); - if (this.dsp.biquad?.filter) this.emit('biquad', this.dsp.biquad?.filter); - if (this.dsp.equalizer) this.emit('eqBands', this.dsp.equalizer.getEQ()); - if (this.dsp.volume) this.emit('volume', this.dsp.volume.volume); - if (this.dsp.resampler) this.emit('sampleRate', this.dsp.resampler.targetSampleRate); - }; - - this.dsp.onError = (e) => this.emit('error', e as AudioPlayerError); - - this.voiceConnection - .on(VoiceConnectionStatus.Disconnected, async (oldState, newState) => { - if (newState.reason === VoiceConnectionDisconnectReason.Manual) { - this.destroy(); - return; - } - - if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) { - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Connecting, this.connectionTimeout); - } catch { - try { - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.destroy(); - } catch (err) { - this.emit('error', err as AudioPlayerError); - } - } - } else if (this.voiceConnection.rejoinAttempts < 5) { - await Util.wait((this.voiceConnection.rejoinAttempts + 1) * 5000); - this.voiceConnection.rejoin(); - } else { - try { - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.destroy(); - } catch (err) { - this.emit('error', err as AudioPlayerError); - } - } - }) - .on(VoiceConnectionStatus.Destroyed, () => { - this.end(); - this.queue.emit(GuildQueueEvent.connectionDestroyed, this.queue); - }); - - this.audioPlayer.on('stateChange', (oldState, newState) => { - if (oldState.status !== AudioPlayerStatus.Paused && newState.status === AudioPlayerStatus.Paused) { - this.queue.emit(GuildQueueEvent.playerPause, this.queue); - } - - if (oldState.status === AudioPlayerStatus.Paused && newState.status !== AudioPlayerStatus.Paused) { - this.queue.emit(GuildQueueEvent.playerResume, this.queue); - } - - if (newState.status === AudioPlayerStatus.Playing) { - if (oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering) { - return this.emit('start', this.audioResource!); - } - } else if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { - this.emit('finish', this.audioResource!); - this.dsp.destroy(); - this.audioResource = null; - } - }); - - this.voiceConnection.subscribe(this.audioPlayer); - } - - /** - * Check if the player has been paused manually - */ - get paused() { - return this.audioPlayer.state.status === AudioPlayerStatus.Paused; - } - - set paused(val: boolean) { - val ? this.pause(true) : this.resume(); - } - - /** - * Whether or not the player is currently paused automatically or manually. - */ - isPaused() { - return this.paused || this.audioPlayer.state.status === AudioPlayerStatus.AutoPaused; - } - - /** - * Whether or not the player is currently buffering - */ - isBuffering() { - return this.audioPlayer.state.status === AudioPlayerStatus.Buffering; - } - - /** - * Whether or not the player is currently playing - */ - isPlaying() { - return this.audioPlayer.state.status === AudioPlayerStatus.Playing; - } - - /** - * Whether or not the player is currently idle - */ - isIdle() { - return this.audioPlayer.state.status === AudioPlayerStatus.Idle; - } - - /** - * Whether or not the voice connection has been destroyed - */ - isDestroyed() { - return this.voiceConnection.state.status === VoiceConnectionStatus.Destroyed; - } - - /** - * Whether or not the voice connection has been destroyed - */ - isDisconnected() { - return this.voiceConnection.state.status === VoiceConnectionStatus.Disconnected; - } - - /** - * Whether or not the voice connection is ready to play - */ - isReady() { - return this.voiceConnection.state.status === VoiceConnectionStatus.Ready; - } - - /** - * Whether or not the voice connection is signalling - */ - isSignalling() { - return this.voiceConnection.state.status === VoiceConnectionStatus.Signalling; - } - - /** - * Whether or not the voice connection is connecting - */ - isConnecting() { - return this.voiceConnection.state.status === VoiceConnectionStatus.Connecting; - } - - /** - * Creates stream - * @param {Readable} src The stream source - * @param {object} [ops] Options - * @returns {AudioResource} - */ - async createStream(src: Readable, ops?: CreateStreamOps) { - if (!ops?.disableFilters && this.queue.hasDebugger) this.queue.debug('Initiating DSP filters pipeline...'); - const stream = !ops?.disableFilters - ? this.dsp.create(src, { - dsp: { - filters: ops?.defaultFilters, - disabled: ops?.disableFilters - }, - biquad: ops?.biquadFilter - ? { - filter: ops.biquadFilter, - disabled: ops?.disableBiquad - } - : undefined, - resampler: { - targetSampleRate: ops?.sampleRate, - disabled: ops?.disableResampler - }, - equalizer: { - bandMultiplier: ops?.eq, - disabled: ops?.disableEqualizer - }, - volume: { - volume: ops?.volume, - disabled: ops?.disableVolume - } - }) - : src; - - if (this.queue.hasDebugger) this.queue.debug('Executing onAfterCreateStream hook...'); - const postStream = await this.queue.onAfterCreateStream?.(stream, this.queue).catch( - () => - ({ - stream: stream, - type: ops?.type ?? StreamType.Arbitrary - } as PostProcessedResult) - ); - - if (this.queue.hasDebugger) this.queue.debug('Preparing AudioResource...'); - this.audioResource = createAudioResource(postStream?.stream ?? stream, { - inputType: postStream?.type ?? ops?.type ?? StreamType.Arbitrary, - metadata: ops?.data, - // volume controls happen from AudioFilter DSP utility - inlineVolume: false - }); - - return this.audioResource; - } - - public get resampler() { - return this.dsp?.resampler; - } - - public get filters() { - return this.dsp?.filters; - } - - public get biquad() { - return this.dsp?.biquad || null; - } - - public get equalizer() { - return this.dsp?.equalizer || null; - } - - /** - * The player status - * @type {AudioPlayerStatus} - */ - get status() { - return this.audioPlayer.state.status; - } - - /** - * Disconnects from voice - * @returns {void} - */ - disconnect() { - try { - if (this.audioPlayer) this.audioPlayer.stop(true); - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.voiceConnection.destroy(); - } catch {} // eslint-disable-line no-empty - } - - /** - * Destroys this dispatcher - */ - public destroy() { - this.disconnect(); - this.audioPlayer.removeAllListeners(); - this.voiceConnection.removeAllListeners(); - this.dsp.destroy(); - this.audioResource = null; - this.emit('destroyed'); - } - - /** - * Stops the player - * @returns {void} - */ - end() { - try { - this.audioPlayer.stop(); - this.dsp.destroy(); - } catch { - // - } - } - - /** - * Pauses the stream playback - * @param {boolean} [interpolateSilence=false] If true, the player will play 5 packets of silence after pausing to prevent audio glitches. - * @returns {boolean} - */ - pause(interpolateSilence?: boolean) { - const success = this.audioPlayer.pause(interpolateSilence); - return success; - } - - /** - * Resumes the stream playback - * @returns {boolean} - */ - resume() { - const success = this.audioPlayer.unpause(); - return success; - } - - /** - * Play stream - * @param {AudioResource} [resource=this.audioResource] The audio resource to play - * @param {boolean} [opus=false] Whether or not to use opus - * @returns {Promise} - */ - async playStream(resource: AudioResource = this.audioResource!) { - if (!resource) { - throw Exceptions.ERR_NO_AUDIO_RESOURCE(); - } - if (resource.ended) { - return void this.emit('finish', resource); - } - if (!this.audioResource) this.audioResource = resource; - if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) { - try { - await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, this.connectionTimeout); - } catch (err) { - return void this.emit('error', err as AudioPlayerError); - } - } - - try { - this.audioPlayer.play(resource); - } catch (e) { - this.emit('error', e as AudioPlayerError); - } - - return this; - } - - /** - * Sets playback volume - * @param {number} value The volume amount - * @returns {boolean} - */ - setVolume(value: number) { - if (!this.dsp.volume) return false; - return this.dsp.volume.setVolume(value); - } - - /** - * The current volume - * @type {number} - */ - get volume() { - if (!this.dsp.volume) return 100; - return this.dsp.volume.volume; - } - - /** - * The playback time - * @type {number} - */ - get streamTime() { - if (!this.audioResource) return 0; - return this.audioResource.playbackDuration; - } -} - -export { StreamDispatcher as StreamDispatcher }; diff --git a/packages/discord-player/src/VoiceInterface/VoiceUtils.ts b/packages/discord-player/src/VoiceInterface/VoiceUtils.ts deleted file mode 100644 index d7c5658c5d..0000000000 --- a/packages/discord-player/src/VoiceInterface/VoiceUtils.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { VoiceChannel, StageChannel, Snowflake } from 'discord.js'; -import { DiscordGatewayAdapterCreator, joinVoiceChannel, VoiceConnection, getVoiceConnection, VoiceConnectionStatus, AudioPlayer } from 'discord-voip'; -import { StreamDispatcher } from './StreamDispatcher'; -import { Collection } from '@discord-player/utils'; -import { GuildQueue } from '../queue'; -import type { Player } from '../Player'; -import { Exceptions } from '../errors'; - -class VoiceUtils { - /** - * Voice connection cache to store voice connections of the Player components. - * This property is deprecated and will be removed in the future. - * It only exists for compatibility reasons. - * @deprecated - */ - public cache: Collection = new Collection(); - - /** - * The voice utils constructor - */ - constructor(public player: Player) {} - - /** - * Joins a voice channel, creating basic stream dispatch manager - * @param {StageChannel|VoiceChannel} channel The voice channel - * @param {object} [options] Join options - * @returns {Promise} - */ - public async connect( - channel: VoiceChannel | StageChannel, - options?: { - deaf?: boolean; - maxTime?: number; - queue: GuildQueue; - audioPlayer?: AudioPlayer; - group?: string; - } - ): Promise { - if (!options?.queue) throw Exceptions.ERR_NO_GUILD_QUEUE(); - const conn = await this.join(channel, options); - const sub = new StreamDispatcher(conn, channel, options.queue, options.maxTime, options.audioPlayer); - return sub; - } - - /** - * Joins a voice channel - * @param {StageChannel|VoiceChannel} [channel] The voice/stage channel to join - * @param {object} [options] Join options - * @returns {VoiceConnection} - */ - public async join( - channel: VoiceChannel | StageChannel, - options?: { - deaf?: boolean; - maxTime?: number; - group?: string; - } - ) { - const conn = joinVoiceChannel({ - guildId: channel.guild.id, - channelId: channel.id, - adapterCreator: channel.guild.voiceAdapterCreator as unknown as DiscordGatewayAdapterCreator, - selfDeaf: Boolean(options?.deaf), - debug: this.player.events.listenerCount('debug') > 0, - group: options?.group - }); - - return conn; - } - - /** - * Disconnects voice connection - * @param {VoiceConnection} connection The voice connection - * @returns {void} - */ - public disconnect(connection: VoiceConnection | StreamDispatcher) { - if (connection instanceof StreamDispatcher) connection = connection.voiceConnection; - - try { - if (connection.state.status !== VoiceConnectionStatus.Destroyed) return connection.destroy(); - } catch { - // - } - } - - /** - * Returns Discord Player voice connection - * @param {Snowflake} guild The guild id - * @returns {StreamDispatcher} - */ - public getConnection(guild: Snowflake, group?: string) { - return getVoiceConnection(guild, group); - } -} - -export { VoiceUtils }; diff --git a/packages/discord-player/src/adapter.ts b/packages/discord-player/src/adapter.ts new file mode 100644 index 0000000000..fcd2cda2e7 --- /dev/null +++ b/packages/discord-player/src/adapter.ts @@ -0,0 +1,59 @@ +import { unsafe } from './common/types'; +import { getPlayerAdapterContext } from './context'; +import { Player } from './player'; + +export type OnGatewayPacket = (packet: unsafe) => void; + +export interface IAdapter { + metadata: T; + sendPacket: (packet: unsafe) => void; + resolveGuild(guild: string): string; + resolveGuildByChannel(channel: string): string; + resolveChannel(channel: string): string; + resolveUser(user: string): string; + setRequestToSpeak(guild: string, channel: string, value: boolean): void; + isVoiceChannel(guild: string, channel: string): boolean; + isStageChannel(guild: string, channel: string): boolean; + getVoiceChannelMembersCount(guild: string, channel: string): number; +} + +export interface IVoiceStateUpdateData { + guild: string; + channel: string; + user: string; + me: string; + selfDeaf: boolean; + selfMute: boolean; + serverDeaf: boolean; + serverMute: boolean; + suppress: boolean; + memberCount: number; +} + +export type VoiceStateUpdateDispatch = (oldState: IVoiceStateUpdateData, newState: IVoiceStateUpdateData) => void; + +export interface AdapterImpl { + onPacket: OnGatewayPacket; + handleVoiceStateUpdate: VoiceStateUpdateDispatch; +} + +export class Adapter implements AdapterImpl { + public readonly player: Player; + public constructor(private readonly config: IAdapter) { + this.player = getPlayerAdapterContext(); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public onPacket(packet: unsafe): void { + throw new Error('Not implemented'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public handleVoiceStateUpdate(oldState: IVoiceStateUpdateData, newState: IVoiceStateUpdateData): void { + throw new Error('Not implemented'); + } +} + +export function createAdapter(adapterConfig: IAdapter): Adapter { + return new Adapter(adapterConfig); +} diff --git a/packages/discord-player/src/common/EventEmitter.ts b/packages/discord-player/src/common/EventEmitter.ts new file mode 100644 index 0000000000..c7683e1d21 --- /dev/null +++ b/packages/discord-player/src/common/EventEmitter.ts @@ -0,0 +1,97 @@ +import { type DefaultListener, type ListenerSignature, EventEmitter as IEventEmitter } from '@discord-player/utils'; + +export type DebugCallback = (message: string) => void; + +export type WithDebugger> = L & { debug: DebugCallback }; + +export const DEBUG_EVENT = 'debug'; + +export class EventEmitter = DefaultListener> extends IEventEmitter> { + #hasDebugger = false; + #debug: DebugCallback | null = null; + + public constructor(public requiredEvents: Array = []) { + super(); + } + + public get debug() { + return this.#debug; + } + + public on>(name: K, listener: WithDebugger[K]) { + if (name === DEBUG_EVENT) { + this.#hasDebugger = true; + this.#setupDebugCallback(); + } + + return super.on(name, listener); + } + + public once>(name: K, listener: WithDebugger[K]) { + if (name === DEBUG_EVENT) { + this.#hasDebugger = true; + this.#setupDebugCallback(); + } + + return super.once(name, listener); + } + + public addListener>(name: K, listener: WithDebugger[K]) { + if (name === DEBUG_EVENT) { + this.#hasDebugger = true; + this.#setupDebugCallback(); + } + + return super.addListener(name, listener); + } + + public off>(name: K, listener: WithDebugger[K]) { + this.#hasDebugger = this.listenerCount(DEBUG_EVENT) > 0; + this.#setupDebugCallback(); + + return super.off(name, listener); + } + + public removeListener>(name: K, listener: WithDebugger[K]) { + this.#hasDebugger = this.listenerCount(DEBUG_EVENT) > 0; + this.#setupDebugCallback(); + + return super.removeListener(name, listener); + } + + public removeAllListeners>(name?: K) { + this.#hasDebugger = this.listenerCount(DEBUG_EVENT) > 0; + this.#setupDebugCallback(); + + return super.removeAllListeners(name); + } + + public emit>(name: K, ...args: Parameters[K]>) { + if (this.requiredEvents.includes(name as keyof L) && !this.eventNames().includes(name)) { + // eslint-disable-next-line no-console + console.error(...args); + process.emitWarning( + `No event listener found for event "${String(name)}". Events ${this.requiredEvents.map((m) => `"${String(m)}"`).join(', ')} must have event listeners.`, + 'UnhandledEventsWarning' + ); + return false; + } + + return super.emit(name, ...args); + } + + public get hasDebugger() { + return this.#hasDebugger; + } + + #setupDebugCallback() { + const hasDebugger = this.listenerCount(DEBUG_EVENT) > 0; + + if (hasDebugger && !this.#debug) { + // @ts-expect-error + this.#debug = (message: string) => this.emit(DEBUG_EVENT, message); + } else if (!hasDebugger && this.#debug) { + this.#debug = null; + } + } +} diff --git a/packages/discord-player/src/common/types.ts b/packages/discord-player/src/common/types.ts new file mode 100644 index 0000000000..d29b6e7ae7 --- /dev/null +++ b/packages/discord-player/src/common/types.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type unsafe = any; diff --git a/packages/discord-player/src/context.ts b/packages/discord-player/src/context.ts new file mode 100644 index 0000000000..897f1bc8eb --- /dev/null +++ b/packages/discord-player/src/context.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import type { Player, PlayerAdapterInterface } from './player'; +import { unsafe } from './common/types'; + +const PlayerAdapterContext = new AsyncLocalStorage>(); + +export function getPlayerAdapterContext(): Player { + const ctx = PlayerAdapterContext.getStore(); + + if (!ctx) throw new Error('No player adapter context found'); + + return ctx; +} + +export function setPlayerAdapterContext(player: Player, adapterInterface: PlayerAdapterInterface) { + return PlayerAdapterContext.run(player, adapterInterface); +} diff --git a/packages/discord-player/src/errors/index.ts b/packages/discord-player/src/errors/index.ts deleted file mode 100644 index f635dd7cb8..0000000000 --- a/packages/discord-player/src/errors/index.ts +++ /dev/null @@ -1,178 +0,0 @@ -const DiscordPlayerErrors = { - ERR_OUT_OF_SPACE: { - name: 'ERR_OUT_OF_SPACE', - type: RangeError, - createError(target: string, capacity: number, total: number) { - return `[${this.name}] Max capacity reached for ${target} (Capacity ${capacity}/Total ${total})`; - } - }, - ERR_INVALID_ARG_TYPE: { - name: 'ERR_INVALID_ARG_TYPE', - type: TypeError, - createError(target: string, expectation: string, found: string) { - return `[${this.name}] Expected ${target} to be "${expectation}", received "${found}"`; - } - }, - ERR_NO_RESULT: { - name: 'ERR_NO_RESULT', - type: Error, - createError(message: string) { - return `[${this.name}] ${message}`; - } - }, - ERR_NOT_IMPLEMENTED: { - name: 'ERR_NOT_IMPLEMENTED', - type: Error, - createError(target: string) { - return `[${this.name}] ${target} is not yet implemented`; - } - }, - ERR_NOT_EXISTING: { - name: 'ERR_NOT_EXISTING', - type: Error, - createError(target: string) { - return `[${this.name}] ${target} does not exist`; - } - }, - ERR_OUT_OF_RANGE: { - name: 'ERR_OUT_OF_RANGE', - type: RangeError, - createError(target: string, value: string, minimum: string, maximum: string) { - return `[${this.name}] ${target} is out of range (Expected minimum ${maximum} and maximum ${maximum}, got ${value})`; - } - }, - ERR_NO_VOICE_CONNECTION: { - name: 'ERR_NO_VOICE_CONNECTION', - type: Error, - createError(message?: string) { - return `[${this.name}] ` + (message || 'No voice connection available, maybe connect to a voice channel first?'); - } - }, - ERR_VOICE_CONNECTION_DESTROYED: { - name: 'ERR_VOICE_CONNECTION_DESTROYED', - type: Error, - createError() { - return `[${this.name}] ` + 'Cannot use destroyed voice connection'; - } - }, - ERR_NO_VOICE_CHANNEL: { - name: 'ERR_NO_VOICE_CHANNEL', - type: Error, - createError() { - return `[${this.name}] ` + 'Could not get the voice channel'; - } - }, - ERR_INVALID_VOICE_CHANNEL: { - name: 'ERR_INVALID_VOICE_CHANNEL', - type: Error, - createError() { - return `[${this.name}] ` + 'Expected a voice channel'; - } - }, - ERR_NO_RECEIVER: { - name: 'ERR_NO_RECEIVER', - type: Error, - createError(message?: string) { - return `[${this.name}] ` + (message || 'No voice receiver is available, maybe connect to a voice channel first?'); - } - }, - ERR_FFMPEG_LOCATOR: { - name: 'ERR_FFMPEG_LOCATOR', - type: Error, - createError(message: string) { - return `[${this.name}] ` + message; - } - }, - ERR_NO_AUDIO_RESOURCE: { - name: 'ERR_NO_AUDIO_RESOURCE', - type: Error, - createError(message?: string) { - return `[${this.name}] ` + (message || 'Expected an audio resource'); - } - }, - ERR_NO_GUILD_QUEUE: { - name: 'ERR_NO_GUILD_QUEUE', - type: Error, - createError(message?: string) { - return `[${this.name}] ` + (message || 'Expected a guild queue'); - } - }, - ERR_NO_GUILD: { - name: 'ERR_NO_GUILD', - type: Error, - createError(message?: string) { - return `[${this.name}] ` + (message || 'Expected a guild'); - } - }, - ERR_INFO_REQUIRED: { - name: 'ERR_INFO_REQUIRED', - type: Error, - createError(target: string, actual: string) { - return `[${this.name}] Expected ${target}, found "${actual}"`; - } - }, - ERR_SERIALIZATION_FAILED: { - name: 'ERR_SERIALIZATION_FAILED', - type: Error, - createError() { - return `[${this.name}]` + "Don't know how to serialize this data"; - } - }, - ERR_DESERIALIZATION_FAILED: { - name: 'ERR_DESERIALIZATION_FAILED', - type: Error, - createError() { - return `[${this.name}]` + "Don't know how to deserialize this data"; - } - }, - ERR_ILLEGAL_HOOK_INVOCATION: { - name: 'ERR_ILLEGAL_HOOK_INVOCATION', - type: Error, - createError(target: string, message?: string) { - return `[${this.name}] ` + `Illegal invocation of ${target} hook.${message ? ` ${message}` : ''}`; - } - } -} as const; - -type FinalException = { - name: O['name']; -} & InstanceType; - -type DiscordPlayerException = { - [K in keyof typeof DiscordPlayerErrors]: (...args: Parameters<(typeof DiscordPlayerErrors)[K]['createError']>) => FinalException<(typeof DiscordPlayerErrors)[K]>; -}; - -const target = {} as DiscordPlayerException; - -const handler: ProxyHandler = { - get(target, p: keyof typeof DiscordPlayerErrors, receiver) { - const err = DiscordPlayerErrors[p]; - - if (!err) return Reflect.get(target, p, receiver); - - return (...args: Parameters<(typeof err)['createError']>) => { - // @ts-expect-error - const exception = new err.type(err.createError(...args)); - const originalName = exception.name; - exception.name = `${err.name} [${originalName}]`; - - return exception; - }; - } -}; - -export const ErrorCodes = (() => { - type ErrCodes = { - -readonly [K in keyof typeof DiscordPlayerErrors]: (typeof DiscordPlayerErrors)[K]['name']; - }; - - const dict = {} as ErrCodes; - - for (const prop in DiscordPlayerErrors) { - // @ts-expect-error - dict[prop] = prop; - } - - return Object.freeze(dict); -})(); -export const Exceptions = new Proxy(target, handler); diff --git a/packages/discord-player/src/extractors/BaseExtractor.ts b/packages/discord-player/src/extractors/BaseExtractor.ts deleted file mode 100644 index 1edd8472bf..0000000000 --- a/packages/discord-player/src/extractors/BaseExtractor.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { User } from 'discord.js'; -import { Readable } from 'stream'; -import { Playlist } from '../fabric/Playlist'; -import { Track } from '../fabric/Track'; -import { PlayerEvents, SearchQueryType } from '../types/types'; -import { ExtractorExecutionContext } from './ExtractorExecutionContext'; -import type { RequestOptions } from 'http'; -import { Exceptions } from '../errors'; -import type { GuildQueueHistory } from '../queue'; - -export type ExtractorStreamable = - | Readable - | string - | { - $fmt: string; - stream: Readable; - }; - -export class BaseExtractor { - /** - * Identifier for this extractor - */ - public static identifier = 'com.discord-player.extractor'; - - /** - * Priority of this extractor. Higher value means higher priority (will be executed first). - */ - public priority = 1; - - /** - * A list of query protocols that this extractor supports. - */ - public protocols: string[] = []; - - /** - * Handle bridge query creation - * @param track The track to build query for - */ - public createBridgeQuery = (track: Track) => `${track.title} by ${track.author} official audio`; - - /** - * Extractor constructor - * @param context Context that instantiated this extractor - * @param options Initialization options for this extractor - */ - public constructor(public context: ExtractorExecutionContext, public options: T = {}) {} - - /** - * Identifier of this extractor - */ - public get identifier() { - return (this.constructor as typeof BaseExtractor).identifier; - } - - /** - * Reconfigures this extractor - * @param options The new options to apply - */ - public async reconfigure(options: T) { - this.options = options; - await this.deactivate(); - await this.activate(); - } - - /** - * This method will be executed when this extractor is activated - */ - public async activate() { - // executed when this extractor is activated - return; - } - - /** - * This method will be executed when this extractor is deactivated - */ - public async deactivate() { - // executed when this extractor is deactivated - return; - } - - /** - * Validate incoming query - * @param query The query to validate - */ - public async validate(query: string, type?: SearchQueryType | null): Promise { - void type; - return false; - } - - /** - * Stream the given track - * @param info The track to stream - */ - public async stream(info: Track): Promise { - void info; - throw Exceptions.ERR_NOT_IMPLEMENTED(`${this.constructor.name}.stream()`); - } - - /** - * Handle the given query - * @param query The query to handle - */ - public async handle(query: string, context: ExtractorSearchContext): Promise { - void context; - throw Exceptions.ERR_NOT_IMPLEMENTED(`${this.constructor.name}.handle()`); - } - - /** - * Get related tracks for the given track - * @param track The track source - */ - public async getRelatedTracks(track: Track, history: GuildQueueHistory): Promise { - void track; - void history; - throw Exceptions.ERR_NOT_IMPLEMENTED(`${this.constructor.name}.getRelatedTracks()`); - } - - /** - * A stream middleware to handle streams before passing it to the player - * @param stream The incoming stream - * @param next The next function - */ - public handlePostStream(stream: Readable, next: NextFunction) { - return next(null, stream); - } - - /** - * Dispatch an event to the player - * @param event The event to dispatch - * @param args The data to dispatch - */ - public emit(event: K, ...args: Parameters) { - return this.context.player.emit(event, ...args); - } - - /** - * Create extractor response - * @param playlist The playlist - * @param tracks The track array - */ - public createResponse(playlist?: Playlist | null, tracks: Track[] = playlist?.tracks || []): ExtractorInfo { - return { playlist: playlist || null, tracks }; - } - - /** - * Write debug message - * @param message The debug message - */ - public debug(message: string) { - return this.context.player.debug(message); - } - - /** - * IP rotator instance, if available - */ - public get routePlanner() { - return this.context.player.routePlanner; - } - - /** - * A flag to indicate `Demuxable` stream support for `opus`/`ogg/opus`/`webm/opus` formats. - */ - public get supportsDemux() { - return !!this.context.player.options.skipFFmpeg; - } -} - -export type NextFunction = (error?: Error | null, stream?: Readable) => void; - -export interface ExtractorInfo { - playlist: Playlist | null; - tracks: Track[]; -} - -export interface ExtractorSearchContext { - type?: SearchQueryType | null; - requestedBy?: User | null; - requestOptions?: RequestOptions; - protocol?: string | null; -} diff --git a/packages/discord-player/src/extractors/ExtractorExecutionContext.ts b/packages/discord-player/src/extractors/ExtractorExecutionContext.ts deleted file mode 100644 index 96974559e0..0000000000 --- a/packages/discord-player/src/extractors/ExtractorExecutionContext.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { Player } from '../Player'; -import { Collection } from '@discord-player/utils'; -import { BaseExtractor } from './BaseExtractor'; -import { Util } from '../utils/Util'; -import { PlayerEventsEmitter } from '../utils/PlayerEventsEmitter'; -import { TypeUtil } from '../utils/TypeUtil'; - -// prettier-ignore -const knownExtractorKeys = [ - 'SpotifyExtractor', - 'AppleMusicExtractor', - 'SoundCloudExtractor', - 'YouTubeExtractor', - 'VimeoExtractor', - 'ReverbnationExtractor', - 'AttachmentExtractor' -] as const; -const knownExtractorLib = '@discord-player/extractor'; - -export type ExtractorLoaderOptionDict = { - // @ts-ignore types - [K in (typeof knownExtractorKeys)[number]]?: ConstructorParameters[1]; -}; - -export interface ExtractorExecutionEvents { - /** - * Emitted when a extractor is registered - * @param context The context where extractor was registered - * @param extractor The extractor that was registered - */ - registered: (context: ExtractorExecutionContext, extractor: BaseExtractor) => unknown; - /** - * Emitted when a extractor is unregistered - * @param context The context where extractor was unregistered - * @param extractor The extractor that was unregistered - */ - unregistered: (context: ExtractorExecutionContext, extractor: BaseExtractor) => unknown; - /** - * Emitted when a extractor is activated - * @param context The context where this event occurred - * @param extractor The extractor which was activated - */ - activate: (context: ExtractorExecutionContext, extractor: BaseExtractor) => unknown; - /** - * Emitted when a extractor is deactivated - * @param context The context where this event occurred - * @param extractor The extractor which was deactivated - */ - deactivate: (context: ExtractorExecutionContext, extractor: BaseExtractor) => unknown; - /** - * Emitted when a extractor fails to activate/deactivate - * @param context The context where this event occurred - * @param extractor The extractor which was deactivated - */ - error: (context: ExtractorExecutionContext, extractor: BaseExtractor, error: Error) => unknown; -} - -export class ExtractorExecutionContext extends PlayerEventsEmitter { - /** - * The extractors store - */ - public store = new Collection(); - - public constructor(public player: Player) { - super(['error']); - } - - /** - * Load default extractors from `@discord-player/extractor` - */ - public async loadDefault(filter?: (ext: (typeof knownExtractorKeys)[number]) => boolean | null, options?: ExtractorLoaderOptionDict) { - const mod = await Util.import(knownExtractorLib); - if (mod.error) return { success: false, error: mod.error as Error }; - - (filter ? knownExtractorKeys.filter(filter) : knownExtractorKeys).forEach((key) => { - if (!mod.module[key]) return; - // @ts-ignore types - this.register(mod.module[key], options?.[key] || {}); - }); - - return { success: true, error: null }; - } - - /** - * Validate if the given extractor is registered - * @param identifier The extractor identifier - */ - public isRegistered(identifier: string) { - return this.store.has(identifier); - } - - /** - * The size of registered extractors - */ - public get size() { - return this.store.size; - } - - /** - * Get single extractor - * @param identifier The extractor to get - */ - public get(identifier: string) { - return this.store.get(identifier); - } - - /** - * Register single extractor - * @param _extractor The extractor to register - * @param options Options supplied to the extractor - */ - public async register>(_extractor: T, options: ConstructorParameters['1']): Promise | null> { - if (typeof _extractor.identifier !== 'string' || this.store.has(_extractor.identifier)) return null; - const extractor = new _extractor(this, options); - - // @ts-ignore - if (this.player.options.bridgeProvider) options.bridgeProvider ??= this.player.options.bridgeProvider; - - try { - this.store.set(_extractor.identifier, extractor); - if (this.player.hasDebugger) this.player.debug(`${_extractor.identifier} extractor loaded!`); - this.emit('registered', this, extractor); - await extractor.activate(); - if (this.player.hasDebugger) this.player.debug(`${_extractor.identifier} extractor activated!`); - this.emit('activate', this, extractor); - return extractor as unknown as InstanceType; - } catch (e) { - this.store.delete(_extractor.identifier); - if (this.player.hasDebugger) this.player.debug(`${_extractor.identifier} extractor failed to activate! Error: ${e}`); - this.emit('error', this, extractor, e as Error); - return null; - } - } - - /** - * Unregister single extractor - * @param _extractor The extractor to unregister - */ - public async unregister(_extractor: K) { - const extractor = typeof _extractor === 'string' ? this.store.get(_extractor) : this.store.find((r) => r === _extractor); - if (!extractor) return; - - try { - const key = extractor.identifier || this.store.findKey((e) => e === extractor)!; - this.store.delete(key); - if (this.player.hasDebugger) this.player.debug(`${extractor.identifier} extractor disabled!`); - this.emit('unregistered', this, extractor); - await extractor.deactivate(); - if (this.player.hasDebugger) this.player.debug(`${extractor.identifier} extractor deactivated!`); - this.emit('deactivate', this, extractor); - } catch (e) { - if (this.player.hasDebugger) this.player.debug(`${extractor.identifier} extractor failed to deactivate!`); - this.emit('error', this, extractor, e as Error); - } - } - - /** - * Unregister all extractors - */ - public async unregisterAll() { - try { - await Promise.all(this.store.map((e) => this.unregister(e))); - } catch { - // do nothing - } - } - - /** - * Run all the extractors - * @param fn The runner function - * @param filterBlocked Filter blocked extractors - */ - public async run(fn: ExtractorExecutionFN, filterBlocked = true) { - const blocked = this.player.options.blockExtractors ?? []; - - if (!this.store.size) { - Util.warn('Skipping extractors execution since zero extractors were registered', 'NoExtractors'); - return; - } - - // sort by priority so that extractors with higher priority are executed first - const extractors = this.store.sort((a, b) => b.priority - a.priority); - - let err: Error | null = null, - lastExt: BaseExtractor | null = null; - - for (const ext of extractors.values()) { - if (filterBlocked && blocked.some((e) => e === ext.identifier)) continue; - if (this.player.hasDebugger) this.player.debug(`Executing extractor ${ext.identifier}...`); - const result = await fn(ext).then( - (res) => { - return res; - }, - (e) => { - if (this.player.hasDebugger) this.player.debug(`Extractor ${ext.identifier} failed with error: ${e}`); - - return TypeUtil.isError(e) ? e : new Error(`${e}`); - } - ); - - lastExt = ext; - - if (result && !TypeUtil.isError(result)) { - if (this.player.hasDebugger) this.player.debug(`Extractor ${ext.identifier} executed successfully!`); - - return { - extractor: ext, - error: null, - result - } as ExtractorExecutionResult; - } else if (TypeUtil.isError(result)) { - err = result; - } - } - - if (err) - return { - extractor: lastExt!, - error: err, - result: false - } as ExtractorExecutionResult; - } - - /** - * Check if extractor is disabled - */ - public isDisabled(identifier: string) { - return this.player.options.blockExtractors?.includes(identifier) ?? false; - } - - /** - * Check if extractor is enabled - */ - public isEnabled(identifier: string) { - return !this.isDisabled(identifier); - } - - /** - * Resolve extractor identifier - */ - public resolveId(resolvable: ExtractorResolvable) { - return typeof resolvable === 'string' ? resolvable : resolvable.identifier; - } - - /** - * Resolve extractor - */ - public resolve(resolvable: ExtractorResolvable) { - return typeof resolvable === 'string' ? this.get(resolvable) : resolvable; - } -} - -export interface ExtractorExecutionResult { - extractor: BaseExtractor; - error: Error | null; - result: T; -} - -export type ExtractorExecutionFN = (extractor: BaseExtractor) => Promise; - -export type ExtractorResolvable = string | BaseExtractor; diff --git a/packages/discord-player/src/fabric/Playlist.ts b/packages/discord-player/src/fabric/Playlist.ts deleted file mode 100644 index 056e1e5777..0000000000 --- a/packages/discord-player/src/fabric/Playlist.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { Player, PlayerNodeInitializationResult, PlayerNodeInitializerOptions } from '../Player'; -import { Track } from './Track'; -import { PlaylistInitData, PlaylistJSON, TrackJSON, TrackSource } from '../types/types'; -import { Util } from '../utils/Util'; -import { GuildVoiceChannelResolvable } from 'discord.js'; -import { SerializedType, tryIntoThumbnailString } from '../utils/serde'; -import { TypeUtil } from '../utils/TypeUtil'; -import { Exceptions } from '../errors'; - -export type SerializedPlaylist = ReturnType; - -export class Playlist { - public readonly player: Player; - public tracks: Track[]; - public title: string; - public description: string; - public thumbnail: string; - public type: 'album' | 'playlist'; - public source: TrackSource; - public author: { - name: string; - url: string; - }; - public id: string; - public url: string; - public readonly rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - - /** - * Playlist constructor - * @param {Player} player The player - * @param {PlaylistInitData} data The data - */ - constructor(player: Player, data: PlaylistInitData) { - /** - * The player - * @name Playlist#player - * @type {Player} - * @readonly - */ - this.player = player; - - /** - * The tracks in this playlist - * @name Playlist#tracks - * @type {Track[]} - */ - this.tracks = data.tracks ?? []; - - /** - * The author of this playlist - * @name Playlist#author - * @type {object} - */ - this.author = data.author; - - /** - * The description - * @name Playlist#description - * @type {string} - */ - this.description = data.description; - - /** - * The thumbnail of this playlist - * @name Playlist#thumbnail - * @type {string} - */ - this.thumbnail = data.thumbnail; - - /** - * The playlist type: - * - `album` - * - `playlist` - * @name Playlist#type - * @type {string} - */ - this.type = data.type; - - /** - * The source of this playlist: - * - `youtube` - * - `soundcloud` - * - `spotify` - * - `arbitrary` - * @name Playlist#source - * @type {string} - */ - this.source = data.source; - - /** - * The playlist id - * @name Playlist#id - * @type {string} - */ - this.id = data.id; - - /** - * The playlist url - * @name Playlist#url - * @type {string} - */ - this.url = data.url; - - /** - * The playlist title - * @type {string} - */ - this.title = data.title; - - /** - * @name Playlist#rawPlaylist - * @type {any} - * @readonly - */ - } - - *[Symbol.iterator]() { - yield* this.tracks; - } - - /** - * Estimated duration of this playlist - */ - public get estimatedDuration() { - return this.tracks.reduce((p, c) => p + c.durationMS, 0); - } - - /** - * Formatted estimated duration of this playlist - */ - public get durationFormatted() { - return Util.buildTimeCode(Util.parseMS(this.estimatedDuration)); - } - - /** - * JSON representation of this playlist - * @param {boolean} [withTracks=true] If it should build json with tracks - * @returns {PlaylistJSON} - */ - toJSON(withTracks = true) { - const payload = { - id: this.id, - url: this.url, - title: this.title, - description: this.description, - thumbnail: this.thumbnail, - type: this.type, - source: this.source, - author: this.author, - tracks: [] as TrackJSON[] - }; - - if (withTracks) payload.tracks = this.tracks.map((m) => m.toJSON(true)); - - return payload as PlaylistJSON; - } - - /** - * Serialize this playlist into reconstructable data - */ - public serialize() { - return { - tracks: this.tracks.map((m) => m.serialize()), - title: this.title, - description: this.description, - thumbnail: TypeUtil.isString(this.thumbnail) ? this.thumbnail : tryIntoThumbnailString(this.thumbnail), - type: this.type, - source: this.source, - author: this.author, - id: this.id, - url: this.url, - $type: SerializedType.Playlist, - $encoder_version: '[VI]{{inject}}[/VI]' - }; - } - - /** - * Deserialize this playlist from serialized data - * @param player Player instance - * @param data Serialized data - */ - public static fromSerialized(player: Player, data: SerializedPlaylist) { - if (data.$type !== SerializedType.Playlist) throw Exceptions.ERR_INVALID_ARG_TYPE('data', 'SerializedPlaylist', 'malformed data'); - return new Playlist(player, { - ...data, - tracks: data.tracks.map((m) => Track.fromSerialized(player, m)) - }); - } - - /** - * Play this playlist to the given voice channel. If queue exists and another track is being played, this playlist will be added to the queue. - * @param channel Voice channel on which this playlist shall be played - * @param options Node initialization options - */ - public async play(channel: GuildVoiceChannelResolvable, options?: PlayerNodeInitializerOptions): Promise> { - const fn = this.player.play.bind(this.player); - - return await fn(channel, this, options); - } -} diff --git a/packages/discord-player/src/fabric/SearchResult.ts b/packages/discord-player/src/fabric/SearchResult.ts deleted file mode 100644 index 5cdde45d43..0000000000 --- a/packages/discord-player/src/fabric/SearchResult.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { User } from 'discord.js'; -import { BaseExtractor } from '../extractors/BaseExtractor'; -import { Player } from '../Player'; -import { QueryExtractorSearch, QueryType, SearchQueryType } from '../types/types'; -import { Playlist } from './Playlist'; -import { Track } from './Track'; - -export interface SearchResultData { - query: string; - queryType?: SearchQueryType | QueryExtractorSearch | null; - extractor?: BaseExtractor | null; - playlist?: Playlist | null; - tracks?: Track[]; - requestedBy?: User | null; -} - -export class SearchResult { - public constructor(public player: Player, private _data: SearchResultData) { - this._data.tracks?.forEach((track) => { - track.extractor ??= this._data.extractor || null; - track.requestedBy ??= _data.requestedBy || null; - }); - } - - public setQueryType(type: SearchQueryType | QueryExtractorSearch) { - this._data.queryType = type; - return this; - } - - public setRequestedBy(user: User) { - this._data.requestedBy = user; - this._data.tracks?.forEach((track) => { - track.requestedBy = user; - }); - return this; - } - - public setExtractor(extractor: BaseExtractor) { - this._data.extractor = extractor; - this._data.tracks?.forEach((track) => { - track.extractor = extractor; - }); - return this; - } - - public setTracks(tracks: Track[]) { - this._data.tracks = tracks; - return this; - } - - public setQuery(query: string) { - this._data.query = query; - return this; - } - - public setPlaylist(playlist: Playlist) { - this._data.playlist = playlist; - return this; - } - - /** - * The search query - */ - public get query() { - return this._data.query; - } - - /** - * The search query type - */ - public get queryType() { - return this._data.queryType || QueryType.AUTO; - } - - /** - * The extractor - */ - public get extractor() { - return this._data.extractor || null; - } - - /** - * Playlist result - */ - public get playlist() { - return this._data.playlist; - } - - /** - * Tracks result - */ - public get tracks() { - return this._data.tracks || []; - } - - /** - * Requested by - */ - public get requestedBy() { - return this._data.requestedBy || null; - } - - /** - * Re-execute this search - */ - public async execute() { - return this.player.search(this.query, { - searchEngine: this.queryType, - requestedBy: this.requestedBy! - }); - } - - /** - * If this search result is empty - */ - public isEmpty() { - return !this.tracks.length; - } - - /** - * If this search result has playlist - */ - public hasPlaylist() { - return this.playlist != null; - } - - /** - * If this search result has tracks - */ - public hasTracks() { - return this.tracks.length > 0; - } - - /** - * JSON representation of this search - */ - public toJSON() { - return { - query: this.query, - queryType: this.queryType, - playlist: this.playlist?.toJSON(false) || null, - tracks: this.tracks.map((m) => m.toJSON(true)), - extractor: this.extractor?.identifier || null, - requestedBy: this.requestedBy?.toJSON() || null - }; - } -} diff --git a/packages/discord-player/src/fabric/Track.ts b/packages/discord-player/src/fabric/Track.ts deleted file mode 100644 index cfd73450f9..0000000000 --- a/packages/discord-player/src/fabric/Track.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { User, escapeMarkdown, SnowflakeUtil, GuildVoiceChannelResolvable, APIUser } from 'discord.js'; -import { Player, PlayerNodeInitializationResult, PlayerNodeInitializerOptions } from '../Player'; -import { RawTrackData, SearchQueryType, TrackJSON } from '../types/types'; -import { Playlist } from './Playlist'; -import { GuildQueue } from '../queue/GuildQueue'; -import { BaseExtractor } from '../extractors/BaseExtractor'; -import { Collection } from '@discord-player/utils'; -import { TypeUtil } from '../utils/TypeUtil'; -import { SerializedType, tryIntoThumbnailString } from '../utils/serde'; -import { Exceptions } from '../errors'; -import { Util } from '../utils/Util'; - -export type TrackResolvable = Track | string | number; - -export type WithMetadata = T & { - metadata: M; - requestMetadata(): Promise; -}; - -export type SerializedTrack = ReturnType; - -export class Track { - public title: string; - public description: string; - public author: string; - public url: string; - public thumbnail: string; - public duration: string; - public views: number; - public requestedBy: User | null = null; - public playlist?: Playlist; - public queryType: SearchQueryType | null | undefined = null; - public raw: RawTrackData = { - source: 'arbitrary' - } as RawTrackData; - public extractor: BaseExtractor | null = null; - public readonly id = SnowflakeUtil.generate().toString(); - private __metadata: T | null = null; - private __reqMetadataFn: () => Promise; - public cleanTitle: string; - - /** - * Track constructor - * @param player The player that instantiated this Track - * @param data Track data - */ - public constructor(public readonly player: Player, data: Partial>) { - this.title = escapeMarkdown(data.title ?? ''); - this.author = data.author ?? ''; - this.url = data.url ?? ''; - this.thumbnail = data.thumbnail ?? ''; - this.duration = data.duration ?? ''; - this.views = data.views ?? 0; - this.queryType = data.queryType; - this.requestedBy = data.requestedBy || null; - this.playlist = data.playlist; - this.description = `${this.title} by ${this.author}`; - this.raw = Object.assign({}, { source: data.raw?.source ?? data.source }, data.raw ?? data); - this.__metadata = data.metadata ?? null; - this.__reqMetadataFn = data.requestMetadata || (() => Promise.resolve(null)); - this.cleanTitle = data.cleanTitle ?? Util.cleanTitle(this.title, this.source); - } - - /** - * Request metadata for this track - */ - public async requestMetadata() { - const res = await this.__reqMetadataFn(); - - this.setMetadata(res); - - return res; - } - - /** - * Set metadata for this track - */ - public setMetadata(m: T | null) { - this.__metadata = m; - } - - /** - * Metadata of this track - */ - public get metadata() { - return this.__metadata; - } - - /** - * If this track has metadata - */ - public get hasMetadata() { - return this.metadata != null; - } - - /** - * The queue in which this track is located - */ - public get queue(): GuildQueue { - return this.player.nodes.cache.find((q) => q.tracks.some((ab) => ab.id === this.id))!; - } - - /** - * The track duration in millisecond - */ - public get durationMS(): number { - const times = (n: number, t: number) => { - let tn = 1; - for (let i = 0; i < t; i++) tn *= n; - return t <= 0 ? 1000 : tn * 1000; - }; - - return this.duration - .split(':') - .reverse() - .map((m, i) => parseInt(m) * times(60, i)) - .reduce((a, c) => a + c, 0); - } - - /** - * Discord hyperlink representation of this track - */ - public toHyperlink(): string /* not using `[${string}](${string})` yet */ { - return `[${this.title}](${this.url})`; - } - - /** - * Returns source of this track - */ - public get source() { - return this.raw?.source ?? 'arbitrary'; - } - - /** - * String representation of this track - */ - public toString(): string { - return `${this.title} by ${this.author}`; - } - - /** - * Raw JSON representation of this track - */ - public toJSON(hidePlaylist?: boolean) { - return { - id: this.id, - title: this.title, - description: this.description, - author: this.author, - url: this.url, - thumbnail: this.thumbnail, - duration: this.duration, - durationMS: this.durationMS, - views: this.views, - requestedBy: this.requestedBy?.id || null, - playlist: hidePlaylist ? null : this.playlist?.toJSON() ?? null - } as TrackJSON; - } - - /** - * Serialized track data that can be reconstructed - */ - public serialize() { - return { - title: this.title, - description: this.description, - author: this.author, - url: this.url, - thumbnail: TypeUtil.isString(this.thumbnail) ? this.thumbnail : tryIntoThumbnailString(this.thumbnail), - duration: this.duration, - views: this.views ?? 0, - requested_by: this.requestedBy?.toJSON() ?? null, - source: this.source, - live: false, - query_type: this.queryType, - extractor: this.extractor?.identifier ?? null, - metadata: this.metadata, - $type: SerializedType.Track, - $encoder_version: '[VI]{{inject}}[/VI]' - }; - } - - /** - * Construct a track from serialized data - * @param player Player instance - * @param data Serialized data - */ - public static fromSerialized(player: Player, data: ReturnType) { - if (data.$type !== SerializedType.Track) throw Exceptions.ERR_INVALID_ARG_TYPE('data', 'SerializedTrack', 'malformed data'); - const track = new Track(player, { - ...data, - requestedBy: data.requested_by - ? (() => { - const res = data.requested_by as APIUser; - try { - const resolved = player.client.users.resolve(res.id); - if (resolved) return resolved; - if (player.client.users.cache.has(res.id)) return player.client.users.cache.get(res.id)!; - // @ts-expect-error - const user = new User(player.client, res); - return user; - } catch { - return null; - } - })() - : null, - queryType: data.query_type ?? undefined - }); - - track.setMetadata(data.metadata); - - return track; - } - - /** - * Get belonging queues of this track - */ - public getBelongingQueues() { - const nodes = this.player.nodes.cache.filter((node) => node.tracks.some((t) => t.id === this.id)); - - return nodes as Collection>; - } - - /** - * Play this track to the given voice channel. If queue exists and another track is being played, this track will be added to the queue. - * @param channel Voice channel on which this track shall be played - * @param options Node initialization options - */ - public async play(channel: GuildVoiceChannelResolvable, options?: PlayerNodeInitializerOptions): Promise> { - const fn = this.player.play.bind(this.player); - - return await fn(channel, this, options); - } -} diff --git a/packages/discord-player/src/fabric/index.ts b/packages/discord-player/src/fabric/index.ts deleted file mode 100644 index ef20a30649..0000000000 --- a/packages/discord-player/src/fabric/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './Playlist'; -export * from './Track'; -export * from './SearchResult'; -export * from '../utils/AudioFilters'; diff --git a/packages/discord-player/src/hooks/common.ts b/packages/discord-player/src/hooks/common.ts deleted file mode 100644 index 00a76e8e47..0000000000 --- a/packages/discord-player/src/hooks/common.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Guild } from 'discord.js'; -import type { Player } from '../Player'; -import { GuildQueue, NodeResolvable } from '../queue'; -import { instances } from '../utils/__internal__'; -import { Exceptions } from '../errors'; -import { useContext } from './context/async-context'; - -const preferredInstanceKey = '__discord_player_hook_instance_cache__'; - -export const getPlayer = () => { - return instances.get(preferredInstanceKey) || instances.first() || null; -}; - -export interface HooksCtx { - guild: Guild; -} - -/** - * @private - */ -export function useHooksContext(hookName: string) { - const player = getPlayer(); - if (!player) throw Exceptions.ERR_ILLEGAL_HOOK_INVOCATION('discord-player', 'Player instance must be created before using hooks'); - - const context = useContext(player.context); - - if (!context) throw Exceptions.ERR_ILLEGAL_HOOK_INVOCATION(hookName, `${hookName} must be called inside a player context created by .context.provide()`); - - return context; -} - -/** - * Bind a player instance to the hook system, defaults to the first instance. - */ -export const bindHook = (player: Player) => { - instances.set(preferredInstanceKey, player); -}; - -export const getQueue = (node: NodeResolvable) => { - const player = getPlayer(); - if (!player) return null; - - return (player.nodes.resolve(node) as GuildQueue) || null; -}; - -export interface HookDeclarationContext { - getQueue: typeof getQueue; - getPlayer: typeof getPlayer; - instances: typeof instances; -} - -/* eslint-disable @typescript-eslint/no-explicit-any */ -export type HookDeclaration any> = (context: HookDeclarationContext) => T; - -export function createHook any>>(hook: T): ReturnType { - return hook({ - getQueue, - getPlayer, - instances - }) as ReturnType; -} - -/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/packages/discord-player/src/hooks/context/async-context.ts b/packages/discord-player/src/hooks/context/async-context.ts deleted file mode 100644 index 5c99a5eed3..0000000000 --- a/packages/discord-player/src/hooks/context/async-context.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type unsafe = any; - -/** - * The receiver function that will be called when the context is provided - */ -export type ContextReceiver = () => R; - -export class Context { - private storage = new AsyncLocalStorage(); - - public constructor(private defaultValue?: T) {} - - /** - * Exit out of this context - */ - public exit(scope: ContextReceiver) { - this.storage.exit(scope); - } - - /** - * Whether the context is lost - */ - public get isLost() { - return this.storage.getStore() === undefined; - } - - /** - * Get the current value of the context. If the context is lost and no default value is provided, undefined will be returned. - */ - public consume(): T | undefined { - const data = this.storage.getStore(); - - if (data === undefined && this.defaultValue !== undefined) return this.defaultValue; - - return data; - } - - /** - * Run a function within the context of this provider - */ - public provide(value: T, receiver: ContextReceiver): R { - if (value === undefined) { - throw new Error('Context value may not be undefined'); - } - - if (typeof receiver !== 'function') { - throw new Error('Context receiver must be a function'); - } - - return this.storage.run(value, receiver); - } -} - -/** - * Create a new context. The default value is optional. - * @param defaultValue The default value of the context - * @example const userContext = createContext(); - * - * // the value to provide - * const user = { - * id: 1, - * name: 'John Doe' - * }; - * - * // provide the context value to the receiver - * context.provide(user, handler); - * - * function handler() { - * // get the context value - * const { id, name } = useContext(context); - * - * console.log(id, name); // 1, John Doe - * } - */ -export function createContext(defaultValue?: T): Context { - return new Context(defaultValue); -} - -/** - * Get the current value of the context. If the context is lost and no default value is provided, undefined will be returned. - * @param context The context to get the value from - * @example const value = useContext(context); - */ -export function useContext(context: Context): T | undefined { - return context.consume(); -} diff --git a/packages/discord-player/src/hooks/index.ts b/packages/discord-player/src/hooks/index.ts deleted file mode 100644 index d7775601b7..0000000000 --- a/packages/discord-player/src/hooks/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './context/async-context'; -export * from './useHistory'; -export * from './usePlayer'; -export * from './useQueue'; -export * from './useMainPlayer'; -export * from './useMetadata'; -export * from './useTimeline'; -export * from './stream'; -export * from './useVolume'; -export { createHook, type HookDeclaration, type HookDeclarationContext } from './common'; diff --git a/packages/discord-player/src/hooks/stream/index.ts b/packages/discord-player/src/hooks/stream/index.ts deleted file mode 100644 index 3343cc7938..0000000000 --- a/packages/discord-player/src/hooks/stream/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './onAfterCreateStream'; -export * from './onBeforeCreateStream'; diff --git a/packages/discord-player/src/hooks/stream/onAfterCreateStream.ts b/packages/discord-player/src/hooks/stream/onAfterCreateStream.ts deleted file mode 100644 index dfd22f4b36..0000000000 --- a/packages/discord-player/src/hooks/stream/onAfterCreateStream.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { OnAfterCreateStreamHandler } from '../../queue'; -import { getGlobalRegistry } from '../../utils/__internal__'; - -/** - * Global onAfterCreateStream handler - * @param handler The handler callback - */ -export function onAfterCreateStream(handler: OnAfterCreateStreamHandler) { - getGlobalRegistry().set('@[onAfterCreateStream]', handler); -} diff --git a/packages/discord-player/src/hooks/stream/onBeforeCreateStream.ts b/packages/discord-player/src/hooks/stream/onBeforeCreateStream.ts deleted file mode 100644 index 3082d02c57..0000000000 --- a/packages/discord-player/src/hooks/stream/onBeforeCreateStream.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { OnBeforeCreateStreamHandler } from '../../queue'; -import { getGlobalRegistry } from '../../utils/__internal__'; - -/** - * Global onBeforeCreateStream handler - * @param handler The handler callback - */ -export function onBeforeCreateStream(handler: OnBeforeCreateStreamHandler) { - getGlobalRegistry().set('@[onBeforeCreateStream]', handler); -} diff --git a/packages/discord-player/src/hooks/useHistory.ts b/packages/discord-player/src/hooks/useHistory.ts deleted file mode 100644 index df2eb661b0..0000000000 --- a/packages/discord-player/src/hooks/useHistory.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { GuildQueueHistory, NodeResolvable } from '../queue'; -import { getQueue, useHooksContext } from './common'; - -/** - * Fetch guild queue history - * @param node guild queue node resolvable - */ -export function useHistory(): GuildQueueHistory | null; -export function useHistory(node: NodeResolvable): GuildQueueHistory | null; -export function useHistory(node?: NodeResolvable): GuildQueueHistory | null { - const _node = node ?? useHooksContext('useHistory').guild; - - const queue = getQueue(_node); - if (!queue) return null; - - return queue.history; -} diff --git a/packages/discord-player/src/hooks/useMainPlayer.ts b/packages/discord-player/src/hooks/useMainPlayer.ts deleted file mode 100644 index be01b5b850..0000000000 --- a/packages/discord-player/src/hooks/useMainPlayer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Exceptions } from '../errors'; -import { Util } from '../utils/Util'; -import { getPlayer } from './common'; - -/** - * Fetch main player instance - * @deprecated - */ -export function useMasterPlayer() { - Util.warn('useMasterPlayer() hook is deprecated, use useMainPlayer() instead.', 'DeprecationWarning'); - return useMainPlayer(); -} - -/** - * Fetch main player instance - */ -export function useMainPlayer() { - const instance = getPlayer(); - if (!instance) { - throw Exceptions.ERR_ILLEGAL_HOOK_INVOCATION('useMainPlayer', 'This is likely caused by calling "useMainPlayer" hook before creating a player instance.'); - } - - return instance; -} diff --git a/packages/discord-player/src/hooks/useMetadata.ts b/packages/discord-player/src/hooks/useMetadata.ts deleted file mode 100644 index c854e4de1a..0000000000 --- a/packages/discord-player/src/hooks/useMetadata.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TypeUtil } from '../utils/TypeUtil'; -import { NodeResolvable } from '../queue'; -import { getQueue, useHooksContext } from './common'; - -export type SetterFN = (previous: P) => T; -export type MetadataDispatch = readonly [() => T, (metadata: T | SetterFN) => void]; - -/** - * Fetch or manipulate guild queue metadata - * @param node Guild queue node resolvable - */ -export function useMetadata(): MetadataDispatch; -export function useMetadata(node: NodeResolvable): MetadataDispatch; -export function useMetadata(node?: NodeResolvable): MetadataDispatch { - const _node = node ?? useHooksContext('useMetadata').guild; - const queue = getQueue(_node); - const setter = (metadata: T | SetterFN) => { - if (queue) { - if (TypeUtil.isFunction(metadata)) return queue.setMetadata(metadata(queue.metadata)); - return queue.setMetadata(metadata); - } - }; - - const getter = () => { - return queue?.metadata as T; - }; - - return [getter, setter] as const; -} diff --git a/packages/discord-player/src/hooks/usePlayer.ts b/packages/discord-player/src/hooks/usePlayer.ts deleted file mode 100644 index c64e829f79..0000000000 --- a/packages/discord-player/src/hooks/usePlayer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GuildQueuePlayerNode, NodeResolvable } from '../queue'; -import { getQueue, useHooksContext } from './common'; - -/** - * Fetch guild queue player node - * @param node Guild queue node resolvable - */ -export function usePlayer(): GuildQueuePlayerNode | null; -export function usePlayer(node: NodeResolvable): GuildQueuePlayerNode | null; -export function usePlayer(node?: NodeResolvable): GuildQueuePlayerNode | null { - const _node = node ?? useHooksContext('usePlayer').guild; - const queue = getQueue(_node); - if (!queue) return null; - - return queue.node; -} diff --git a/packages/discord-player/src/hooks/useQueue.ts b/packages/discord-player/src/hooks/useQueue.ts deleted file mode 100644 index 1c42a20b0a..0000000000 --- a/packages/discord-player/src/hooks/useQueue.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { GuildQueue, NodeResolvable } from '../queue'; -import { getQueue, useHooksContext } from './common'; - -/** - * Fetch guild queue - * @param node Guild queue node resolvable - */ -export function useQueue(): GuildQueue | null; -export function useQueue(node: NodeResolvable): GuildQueue | null; -export function useQueue(node?: NodeResolvable): GuildQueue | null { - const _node = node ?? useHooksContext('useQueue').guild; - const queue = getQueue(_node); - if (!queue) return null; - - return queue; -} diff --git a/packages/discord-player/src/hooks/useTimeline.ts b/packages/discord-player/src/hooks/useTimeline.ts deleted file mode 100644 index b0641cd0c4..0000000000 --- a/packages/discord-player/src/hooks/useTimeline.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NodeResolvable } from '../queue'; -import { getQueue, useHooksContext } from './common'; - -export interface TimelineDispatcherOptions { - ignoreFilters: boolean; -} - -/** - * Fetch or manipulate current track - * @param node Guild queue node resolvable - * @param options Options for timeline dispatcher - */ -export function useTimeline(node?: NodeResolvable, options?: Partial) { - const _node = node ?? useHooksContext('useTimeline').guild; - const queue = getQueue(_node); - if (!queue) return null; - - return Object.preventExtensions({ - get timestamp() { - return queue.node.getTimestamp(options?.ignoreFilters)!; - }, - get volume() { - return queue.node.volume; - }, - get paused() { - return queue.node.isPaused(); - }, - get track() { - return queue.currentTrack; - }, - pause() { - return queue.node.pause(); - }, - resume() { - return queue.node.resume(); - }, - setVolume(vol: number) { - return queue.node.setVolume(vol); - }, - async setPosition(time: number) { - return queue.node.seek(time); - } - }); -} diff --git a/packages/discord-player/src/hooks/useVolume.ts b/packages/discord-player/src/hooks/useVolume.ts deleted file mode 100644 index 5171ff085e..0000000000 --- a/packages/discord-player/src/hooks/useVolume.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TypeUtil } from '../utils/TypeUtil'; -import { NodeResolvable } from '../queue'; -import { getQueue, useHooksContext } from './common'; - -type SetterFN = (previous: number) => number; -type VolumeDispatch = readonly [() => number, (volume: number | SetterFN) => boolean | undefined]; - -/** - * Fetch or manipulate player volume - * @param node Guild queue node resolvable - */ -export function useVolume(): VolumeDispatch; -export function useVolume(node: NodeResolvable): VolumeDispatch; -export function useVolume(node?: NodeResolvable): VolumeDispatch { - const _node = node ?? useHooksContext('useVolume').guild; - const queue = getQueue(_node); - const setter = (volume: number | SetterFN) => { - if (queue) { - if (TypeUtil.isFunction(volume)) return queue.node.setVolume(volume(queue.node.volume)); - return queue.node.setVolume(volume); - } - }; - - const getter = () => { - return queue?.node.volume as number; - }; - - return [getter, setter] as const; -} diff --git a/packages/discord-player/src/index.ts b/packages/discord-player/src/index.ts index 8b08d900e4..cb0ff5c3b5 100644 --- a/packages/discord-player/src/index.ts +++ b/packages/discord-player/src/index.ts @@ -1,44 +1 @@ -import { version as djsVersion } from 'discord.js'; - -export * from './utils/PlayerEventsEmitter'; -export * from './utils/AudioFilters'; -export * from './extractors/BaseExtractor'; -export * from './extractors/ExtractorExecutionContext'; -export * from './fabric'; -export * from './queue'; -export * from './lrclib/LrcLib'; -export * from './utils/SequentialBucket'; -export * from './VoiceInterface/VoiceUtils'; -export * from './VoiceInterface/StreamDispatcher'; -export * from './utils/Util'; -export * from './utils/TypeUtil'; -export * from './utils/AsyncQueue'; -export * from './types/types'; -export * from './utils/FFmpegStream'; -export * from './utils/QueryCache'; -export * from './utils/QueryResolver'; -export * from '@discord-player/ffmpeg'; -export * from './Player'; -export * from './hooks'; -export * from './utils/IPRotator'; -export * from './utils/serde'; -export { - AudioFilters as PCMAudioFilters, - type BiquadFilters, - FilterType as BiquadFilterType, - type PCMFilters, - Q_BUTTERWORTH, - VolumeTransformer, - BASS_EQ_BANDS, - AF_NIGHTCORE_RATE, - AF_VAPORWAVE_RATE, - FiltersChain -} from '@discord-player/equalizer'; -export { createAudioPlayer, AudioPlayer, type CreateAudioPlayerOptions } from 'discord-voip'; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; - -if (!djsVersion.startsWith('14')) { - process.emitWarning(`Discord.js v${djsVersion} is incompatible with Discord Player v${version}! Please use >=v14.x of Discord.js`); -} +export {}; diff --git a/packages/discord-player/src/lrclib/LrcLib.ts b/packages/discord-player/src/lrclib/LrcLib.ts deleted file mode 100644 index 0781426a7f..0000000000 --- a/packages/discord-player/src/lrclib/LrcLib.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { Exceptions } from '../errors'; -import type { Player } from '../Player'; -import { Util } from '../utils/Util'; -import { SequentialBucket } from '../utils/SequentialBucket'; - -export interface LrcSearchParams { - /** - * The query to search for. Either this or trackName is required. - */ - q?: string; - /** - * The track name to search for. Either this or query is required. - */ - trackName?: string; - /** - * The artist name - */ - artistName?: string; - /** - * The album name - */ - albumName?: string; -} - -export interface LrcGetParams extends Required> { - /** - * The duration of the track - */ - duration: number; -} - -const toSnakeCase = (obj: Record): Record => { - const snakeObj: Record = {}; - - for (const [key, value] of Object.entries(obj)) { - if (value == null) continue; - const newKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); - snakeObj[newKey] = value; - } - - return snakeObj; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const createQuery = (params: any) => new URLSearchParams(toSnakeCase(params)).toString(); - -export interface LrcSearchResult { - /** - * The track id - */ - id: number; - /** - * The track name - */ - name: string; - /** - * The artist name - */ - trackName: string; - /** - * The album name - */ - artistName: string; - /** - * The album name - */ - albumName: string; - /** - * The duration of the track - */ - duration: number; - /** - * The release date of the track - */ - instrumental: boolean; - /** - * The release date of the track - */ - plainLyrics: string; - /** - * The release date of the track - */ - syncedLyrics?: string; -} - -export type LrcGetResult = Omit; - -export class LrcLib { - /** - * The API URL - */ - public api = 'https://lrclib.net/api'; - /** - * The request timeout. Default is 15 seconds. - */ - public timeout = 15_000; - /** - * The request bucket - */ - public bucket = new SequentialBucket(); - - /** - * Creates a new LrcLib instance - * @param {Player} player The player instance - */ - public constructor(public readonly player: Player) {} - - /** - * Sets the request timeout - * @param {number} timeout The timeout in milliseconds - */ - public setRequestTimeout(timeout: number) { - this.timeout = timeout; - } - - /** - * Sets the retry limit. Default is 5. - * @param {number} limit The retry limit - */ - public setRetryLimit(limit: number) { - this.bucket.MAX_RETRIES = limit; - } - - /** - * Gets lyrics - * @param params The get params - */ - public get(params: LrcGetParams) { - const path = `get?${createQuery(params)}`; - - return this.request(path); - } - - /** - * Gets lyrics by ID - * @param id The lyrics ID - */ - public getById(id: `${number}` | number) { - return this.request(`get/${id}`); - } - - /** - * Gets cached lyrics - * @param params The get params - */ - public getCached(params: LrcGetParams) { - const path = `get-cached?${createQuery(params)}`; - - return this.request(path); - } - - /** - * Searches for lyrics - * @param params The search params - */ - public search(params: LrcSearchParams) { - if (!params.q && !params.trackName) { - throw Exceptions.ERR_INVALID_ARG_TYPE('one of q or trackName', 'string', [String(params.q), String(params.trackName)].join(', ')); - } - - const path = `search?${createQuery(params)}`; - - return this.request(path); - } - - /** - * Requests the API - * @param path The path - * @param options The request options - */ - public async request(path: string, options?: RequestInit): Promise { - let timeout: NodeJS.Timeout | null = null; - - const dispatcher = () => { - const controller = new AbortController(); - - timeout = setTimeout(() => { - controller.abort(); - }, this.timeout).unref(); - - const { name, version } = Util.getRuntime(); - - const runtimeVersion = name === 'unknown' ? version : `${name}/${version}`; - - const init: RequestInit = { - method: 'GET', - redirect: 'follow', - signal: controller.signal, - ...options, - headers: { - 'User-Agent': `Discord-Player/${this.player.version} ${runtimeVersion ?? ''}`.trimEnd(), - 'Content-Type': 'application/json', - ...options?.headers - } - }; - - this.player.debug(`[LrcLib] Requesting ${path}`); - - return fetch(`${this.api}${path.startsWith('/') ? path : '/' + path}`, init); - }; - - const res = await this.bucket.enqueue(dispatcher); - - if (timeout) clearTimeout(timeout); - - return res.json(); - } -} diff --git a/packages/discord-player/src/queue/GuildNodeManager.ts b/packages/discord-player/src/queue/GuildNodeManager.ts deleted file mode 100644 index bcbb9246cd..0000000000 --- a/packages/discord-player/src/queue/GuildNodeManager.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { EqualizerBand, PCMFilters, BiquadFilters } from '@discord-player/equalizer'; -import { Collection, QueueStrategy } from '@discord-player/utils'; -import { GuildResolvable } from 'discord.js'; -import { Player } from '../Player'; -import { GuildQueue, OnAfterCreateStreamHandler, OnBeforeCreateStreamHandler } from './GuildQueue'; -import { FiltersName, QueueRepeatMode } from '../types/types'; -import { getGlobalRegistry } from '../utils/__internal__'; -import { Exceptions } from '../errors'; - -export interface GuildNodeCreateOptions { - strategy?: QueueStrategy; - volume?: number; - equalizer?: EqualizerBand[]; - a_filter?: PCMFilters[]; - biquad?: BiquadFilters; - resampler?: number; - disableHistory?: boolean; - onBeforeCreateStream?: OnBeforeCreateStreamHandler; - onAfterCreateStream?: OnAfterCreateStreamHandler; - repeatMode?: QueueRepeatMode; - pauseOnEmpty?: boolean; - leaveOnEmpty?: boolean; - leaveOnEmptyCooldown?: number; - leaveOnEnd?: boolean; - leaveOnEndCooldown?: number; - leaveOnStop?: boolean; - leaveOnStopCooldown?: number; - metadata?: T | null; - selfDeaf?: boolean; - connectionTimeout?: number; - defaultFFmpegFilters?: FiltersName[]; - bufferingTimeout?: number; - noEmitInsert?: boolean; - maxSize?: number; - maxHistorySize?: number; - preferBridgedMetadata?: boolean; - disableVolume?: boolean; - disableEqualizer?: boolean; - disableFilterer?: boolean; - disableBiquad?: boolean; - disableResampler?: boolean; -} - -export type NodeResolvable = GuildQueue | GuildResolvable; - -export class GuildNodeManager { - public cache = new Collection(); - public constructor(public player: Player) {} - - /** - * Create guild queue if it does not exist - * @param guild The guild which will be the owner of the queue - * @param options Queue initializer options - */ - public create(guild: GuildResolvable, options: GuildNodeCreateOptions = {}): GuildQueue { - const server = this.player.client.guilds.resolve(guild); - if (!server) { - throw Exceptions.ERR_NO_GUILD('Invalid or unknown guild'); - } - - if (this.cache.has(server.id)) { - return this.cache.get(server.id) as GuildQueue; - } - - options.strategy ??= 'FIFO'; - options.volume ??= 100; - options.equalizer ??= []; - options.a_filter ??= []; - options.disableHistory ??= false; - options.leaveOnEmpty ??= true; - options.leaveOnEmptyCooldown ??= 0; - options.leaveOnEnd ??= true; - options.leaveOnEndCooldown ??= 0; - options.leaveOnStop ??= true; - options.leaveOnStopCooldown ??= 0; - options.resampler ??= 48000; - options.selfDeaf ??= true; - options.connectionTimeout ??= this.player.options.connectionTimeout; - options.bufferingTimeout ??= 1000; - options.maxSize ??= Infinity; - options.maxHistorySize ??= Infinity; - options.preferBridgedMetadata ??= true; - options.pauseOnEmpty ??= true; - // todo(twlite): maybe disable these by default? - options.disableBiquad ??= false; - options.disableEqualizer ??= false; - options.disableFilterer ??= false; - options.disableVolume ??= false; - options.disableResampler ??= true; - - if (getGlobalRegistry().has('@[onBeforeCreateStream]') && !options.onBeforeCreateStream) { - options.onBeforeCreateStream = getGlobalRegistry().get('@[onBeforeCreateStream]') as OnBeforeCreateStreamHandler; - } - - if (getGlobalRegistry().has('@[onAfterCreateStream]') && !options.onAfterCreateStream) { - options.onAfterCreateStream = getGlobalRegistry().get('@[onAfterCreateStream]') as OnAfterCreateStreamHandler; - } - - const queue = new GuildQueue(this.player, { - guild: server, - queueStrategy: options.strategy, - volume: options.volume, - equalizer: options.equalizer, - filterer: options.a_filter, - biquad: options.biquad, - resampler: options.resampler, - disableHistory: options.disableHistory, - onBeforeCreateStream: options.onBeforeCreateStream, - onAfterCreateStream: options.onAfterCreateStream, - repeatMode: options.repeatMode, - leaveOnEmpty: options.leaveOnEmpty, - leaveOnEmptyCooldown: options.leaveOnEmptyCooldown, - leaveOnEnd: options.leaveOnEnd, - leaveOnEndCooldown: options.leaveOnEndCooldown, - leaveOnStop: options.leaveOnStop, - leaveOnStopCooldown: options.leaveOnStopCooldown, - metadata: options.metadata, - connectionTimeout: options.connectionTimeout ?? 120_000, - selfDeaf: options.selfDeaf, - ffmpegFilters: options.defaultFFmpegFilters ?? [], - bufferingTimeout: options.bufferingTimeout, - noEmitInsert: options.noEmitInsert ?? false, - preferBridgedMetadata: options.preferBridgedMetadata, - maxHistorySize: options.maxHistorySize, - maxSize: options.maxSize, - pauseOnEmpty: options.pauseOnEmpty, - disableBiquad: options.disableBiquad, - disableEqualizer: options.disableEqualizer, - disableFilterer: options.disableFilterer, - disableResampler: options.disableResampler, - disableVolume: options.disableVolume - }); - - this.cache.set(server.id, queue); - - return queue; - } - - /** - * Get existing queue - * @param node Queue resolvable - */ - public get(node: NodeResolvable) { - const queue = this.resolve(node); - if (!queue) return null; - - return (this.cache.get(queue.id) as GuildQueue) || null; - } - - /** - * Check if a queue exists - * @param node Queue resolvable - */ - public has(node: NodeResolvable) { - const id = node instanceof GuildQueue ? node.id : this.player.client.guilds.resolveId(node)!; - return this.cache.has(id); - } - - /** - * Delete queue - * @param node Queue resolvable - */ - public delete(node: NodeResolvable) { - const queue = this.resolve(node); - if (!queue) { - throw Exceptions.ERR_NO_GUILD_QUEUE('Cannot delete non-existing queue'); - } - - queue.setTransitioning(true); - queue.node.stop(true); - queue.connection?.removeAllListeners(); - queue.dispatcher?.removeAllListeners(); - queue.dispatcher?.disconnect(); - queue.timeouts.forEach((tm) => clearTimeout(tm)); - queue.history.clear(); - queue.tracks.clear(); - - return this.cache.delete(queue.id); - } - - /** - * Resolve queue - * @param node Queue resolvable - */ - public resolve(node: NodeResolvable) { - if (node instanceof GuildQueue) { - return node; - } - - return this.cache.get(this.player.client.guilds.resolveId(node)!) as GuildQueue | undefined; - } - - /** - * Resolve queue id - * @param node Queue resolvable - */ - public resolveId(node: NodeResolvable) { - const q = this.resolve(node); - return q?.id || null; - } -} diff --git a/packages/discord-player/src/queue/GuildQueue.ts b/packages/discord-player/src/queue/GuildQueue.ts deleted file mode 100644 index 53f9e9013f..0000000000 --- a/packages/discord-player/src/queue/GuildQueue.ts +++ /dev/null @@ -1,1164 +0,0 @@ -import { Player, PlayerNodeInitializerOptions, TrackLike } from '../Player'; -import { ChannelType, Guild, GuildVoiceChannelResolvable, VoiceBasedChannel, VoiceState } from 'discord.js'; -import { Collection, Queue, QueueStrategy } from '@discord-player/utils'; -import { BiquadFilters, EqualizerBand, PCMFilters } from '@discord-player/equalizer'; -import { Track, TrackResolvable } from '../fabric/Track'; -import { StreamDispatcher } from '../VoiceInterface/StreamDispatcher'; -import { type AudioPlayer, AudioResource, StreamType, VoiceConnection, VoiceConnectionStatus } from 'discord-voip'; -import { Util, VALIDATE_QUEUE_CAP } from '../utils/Util'; -import { Playlist } from '../fabric/Playlist'; -import { GuildQueueHistory } from './GuildQueueHistory'; -import { GuildQueuePlayerNode, StreamConfig } from './GuildQueuePlayerNode'; -import { GuildQueueAudioFilters } from './GuildQueueAudioFilters'; -import { Readable } from 'stream'; -import { FiltersName, QueueRepeatMode, SearchQueryType } from '../types/types'; -import { setTimeout } from 'timers'; -import { GuildQueueStatistics } from './GuildQueueStatistics'; -import { TypeUtil } from '../utils/TypeUtil'; -import { AsyncQueue } from '../utils/AsyncQueue'; -import { Exceptions } from '../errors'; -import { SyncedLyricsProvider } from './SyncedLyricsProvider'; -import { LrcGetResult, LrcSearchResult } from '../lrclib/LrcLib'; - -export interface GuildNodeInit { - guild: Guild; - queueStrategy: QueueStrategy; - equalizer: EqualizerBand[] | boolean; - volume: number | boolean; - biquad: BiquadFilters | boolean | undefined; - resampler: number | boolean; - filterer: PCMFilters[] | boolean; - ffmpegFilters: FiltersName[]; - disableHistory: boolean; - onBeforeCreateStream?: OnBeforeCreateStreamHandler; - onAfterCreateStream?: OnAfterCreateStreamHandler; - repeatMode?: QueueRepeatMode; - leaveOnEmpty: boolean; - leaveOnEmptyCooldown: number; - leaveOnEnd: boolean; - leaveOnEndCooldown: number; - leaveOnStop: boolean; - leaveOnStopCooldown: number; - connectionTimeout: number; - selfDeaf?: boolean; - metadata?: Meta | null; - bufferingTimeout: number; - noEmitInsert: boolean; - maxSize?: number; - maxHistorySize?: number; - preferBridgedMetadata: boolean; - pauseOnEmpty?: boolean; - disableVolume: boolean; - disableEqualizer: boolean; - disableFilterer: boolean; - disableBiquad: boolean; - disableResampler: boolean; -} - -export interface VoiceConnectConfig { - deaf?: boolean; - timeout?: number; - group?: string; - audioPlayer?: AudioPlayer; -} - -export interface PostProcessedResult { - stream: Readable; - type: StreamType; -} - -export type OnBeforeCreateStreamHandler = (track: Track, queryType: SearchQueryType, queue: GuildQueue) => Promise; -export type OnAfterCreateStreamHandler = (stream: Readable, queue: GuildQueue) => Promise; - -export type PlayerTriggeredReason = 'filters' | 'normal'; - -export const GuildQueueEvent = { - /** - * Emitted when audio track is added to the queue - */ - audioTrackAdd: 'audioTrackAdd', - AudioTrackAdd: 'audioTrackAdd', - /** - * Emitted when audio tracks were added to the queue - */ - audioTracksAdd: 'audioTracksAdd', - AudioTracksAdd: 'audioTracksAdd', - /** - * Emitted when audio track is removed from the queue - */ - audioTrackRemove: 'audioTrackRemove', - AudioTrackRemove: 'audioTrackRemove', - /** - * Emitted when audio tracks are removed from the queue - */ - audioTracksRemove: 'audioTracksRemove', - AudioTracksRemove: 'audioTracksRemove', - /** - * Emitted when a connection is created - */ - connection: 'connection', - Connection: 'connection', - /** - * Emitted when a voice connection is destroyed - */ - connectionDestroyed: 'connectionDestroyed', - ConnectionDestroyed: 'connectionDestroyed', - /** - * Emitted when the bot is disconnected from the channel - */ - disconnect: 'disconnect', - Disconnect: 'disconnect', - /** - * Emitted when the queue sends a debug info - */ - debug: 'debug', - Debug: 'debug', - /** - * Emitted when the queue encounters error - */ - error: 'error', - Error: 'error', - /** - * Emitted when the voice channel is empty - */ - emptyChannel: 'emptyChannel', - EmptyChannel: 'emptyChannel', - /** - * Emitted when the queue is empty - */ - emptyQueue: 'emptyQueue', - EmptyQueue: 'emptyQueue', - /** - * Emitted when the audio player starts streaming audio track - */ - playerStart: 'playerStart', - PlayerStart: 'playerStart', - /** - * Emitted when the audio player errors while streaming audio track - */ - playerError: 'playerError', - PlayerError: 'playerError', - /** - * Emitted when the audio player finishes streaming audio track - */ - playerFinish: 'playerFinish', - PlayerFinish: 'playerFinish', - /** - * Emitted when the audio player skips current track - */ - playerSkip: 'playerSkip', - PlayerSkip: 'playerSkip', - /** - * Emitted when the audio player is triggered - */ - playerTrigger: 'playerTrigger', - PlayerTrigger: 'playerTrigger', - /** - * Emitted when the voice state is updated. Consuming this event may disable default voice state update handler if `Player.isVoiceStateHandlerLocked()` returns `false`. - */ - voiceStateUpdate: 'voiceStateUpdate', - VoiceStateUpdate: 'voiceStateUpdate', - /** - * Emitted when volume is updated - */ - volumeChange: 'volumeChange', - VolumeChange: 'volumeChange', - /** - * Emitted when player is paused - */ - playerPause: 'playerPause', - PlayerPause: 'playerPause', - /** - * Emitted when player is resumed - */ - playerResume: 'playerResume', - PlayerResume: 'playerResume', - /** - * Biquad Filters Update - */ - biquadFiltersUpdate: 'biquadFiltersUpdate', - BiquadFiltersUpdate: 'biquadFiltersUpdate', - /** - * Equalizer Update - */ - equalizerUpdate: 'equalizerUpdate', - EqualizerUpdate: 'equalizerUpdate', - /** - * DSP update - */ - dspUpdate: 'dspUpdate', - DSPUpdate: 'dspUpdate', - /** - * Audio Filters Update - */ - audioFiltersUpdate: 'audioFiltersUpdate', - AudioFiltersUpdate: 'audioFiltersUpdate', - /** - * Audio player will play next track - */ - willPlayTrack: 'willPlayTrack', - WillPlayTrack: 'willPlayTrack', - /** - * Emitted when a voice channel is repopulated - */ - channelPopulate: 'channelPopulate', - ChannelPopulate: 'channelPopulate', - /** - * Emitted when a queue is successfully created - */ - queueCreate: 'queueCreate', - QueueCreate: 'queueCreate', - /** - * Emitted when a queue is deleted - */ - queueDelete: 'queueDelete', - QueueDelete: 'queueDelete', - /** - * Emitted when a queue is trying to add similar track for autoplay - */ - willAutoPlay: 'willAutoPlay', - WillAutoPlay: 'willAutoPlay' -} as const; - -export type GuildQueueEvent = (typeof GuildQueueEvent)[keyof typeof GuildQueueEvent]; - -export enum TrackSkipReason { - NoStream = 'ERR_NO_STREAM', - Manual = 'MANUAL', - SEEK_OVER_THRESHOLD = 'SEEK_OVER_THRESHOLD', - Jump = 'JUMPED_TO_ANOTHER_TRACK', - SkipTo = 'SKIP_TO_ANOTHER_TRACK', - HistoryNext = 'HISTORY_NEXT_TRACK' -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface GuildQueueEvents { - /** - * Emitted when audio track is added to the queue - * @param queue The queue where this event occurred - * @param track The track - */ - [GuildQueueEvent.AudioTrackAdd]: (queue: GuildQueue, track: Track) => unknown; - /** - * Emitted when audio tracks were added to the queue - * @param queue The queue where this event occurred - * @param tracks The tracks array - */ - [GuildQueueEvent.AudioTracksAdd]: (queue: GuildQueue, track: Track[]) => unknown; - /** - * Emitted when audio track is removed from the queue - * @param queue The queue where this event occurred - * @param track The track - */ - [GuildQueueEvent.AudioTrackRemove]: (queue: GuildQueue, track: Track) => unknown; - /** - * Emitted when audio tracks are removed from the queue - * @param queue The queue where this event occurred - * @param track The track - */ - [GuildQueueEvent.AudioTracksRemove]: (queue: GuildQueue, track: Track[]) => unknown; - /** - * Emitted when a connection is created - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.Connection]: (queue: GuildQueue) => unknown; - /** - * Emitted when a connection is destroyed - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.ConnectionDestroyed]: (queue: GuildQueue) => unknown; - /** - * Emitted when the bot is disconnected from the channel - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.Disconnect]: (queue: GuildQueue) => unknown; - /** - * Emitted when the queue sends a debug info - * @param queue The queue where this event occurred - * @param message The debug message - */ - [GuildQueueEvent.Debug]: (queue: GuildQueue, message: string) => unknown; - /** - * Emitted when the queue encounters error - * @param queue The queue where this event occurred - * @param error The error - */ - [GuildQueueEvent.Error]: (queue: GuildQueue, error: Error) => unknown; - /** - * Emitted when the voice channel is empty - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.EmptyChannel]: (queue: GuildQueue) => unknown; - /** - * Emitted when the queue is empty - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.EmptyQueue]: (queue: GuildQueue) => unknown; - /** - * Emitted when the audio player starts streaming audio track - * @param queue The queue where this event occurred - * @param track The track that is being streamed - */ - [GuildQueueEvent.PlayerStart]: (queue: GuildQueue, track: Track) => unknown; - /** - * Emitted when the audio player errors while streaming audio track - * @param queue The queue where this event occurred - * @param error The error - * @param track The track that is being streamed - */ - [GuildQueueEvent.PlayerError]: (queue: GuildQueue, error: Error, track: Track) => unknown; - /** - * Emitted when the audio player finishes streaming audio track - * @param queue The queue where this event occurred - * @param track The track that was being streamed - */ - [GuildQueueEvent.PlayerFinish]: (queue: GuildQueue, track: Track) => unknown; - /** - * Emitted when the audio player skips current track - * @param queue The queue where this event occurred - * @param track The track that was skipped - * @param reason The reason for skipping - * @param description The description for skipping - */ - [GuildQueueEvent.PlayerSkip]: (queue: GuildQueue, track: Track, reason: TrackSkipReason, description: string) => unknown; - /** - * Emitted when the audio player is triggered - * @param queue The queue where this event occurred - * @param track The track which was played in this event - */ - [GuildQueueEvent.PlayerTrigger]: (queue: GuildQueue, track: Track, reason: PlayerTriggeredReason) => unknown; - /** - * Emitted when the voice state is updated. Consuming this event may disable default voice state update handler if `Player.isVoiceStateHandlerLocked()` returns `false`. - * @param queue The queue where this event occurred - * @param oldState The old voice state - * @param newState The new voice state - */ - [GuildQueueEvent.VoiceStateUpdate]: (queue: GuildQueue, oldState: VoiceState, newState: VoiceState) => unknown; - /** - * Emitted when audio player is paused - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.PlayerPause]: (queue: GuildQueue) => unknown; - /** - * Emitted when audio player is resumed - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.PlayerResume]: (queue: GuildQueue) => unknown; - /** - * Emitted when audio player's volume is changed - * @param queue The queue where this event occurred - * @param oldVolume The old volume - * @param newVolume The updated volume - */ - [GuildQueueEvent.VolumeChange]: (queue: GuildQueue, oldVolume: number, newVolume: number) => unknown; - /** - * Emitted when equalizer config is updated - * @param queue The queue where this event occurred - * @param oldFilters Old filters - * @param newFilters New filters - */ - [GuildQueueEvent.EqualizerUpdate]: (queue: GuildQueue, oldFilters: EqualizerBand[], newFilters: EqualizerBand[]) => unknown; - /** - * Emitted when biquad filters is updated - * @param queue The queue where this event occurred - * @param oldFilters Old filters - * @param newFilters New filters - */ - [GuildQueueEvent.BiquadFiltersUpdate]: (queue: GuildQueue, oldFilters: BiquadFilters | null, newFilters: BiquadFilters | null) => unknown; - /** - * Emitted when dsp filters is updated - * @param queue The queue where this event occurred - * @param oldFilters Old filters - * @param newFilters New filters - */ - [GuildQueueEvent.DSPUpdate]: (queue: GuildQueue, oldFilters: PCMFilters[], newFilters: PCMFilters[]) => unknown; - /** - * Emitted when ffmpeg audio filters is updated - * @param queue The queue where this event occurred - * @param oldFilters Old filters - * @param newFilters New filters - */ - [GuildQueueEvent.AudioFiltersUpdate]: (queue: GuildQueue, oldFilters: FiltersName[], newFilters: FiltersName[]) => unknown; - - /** - * Emitted before streaming an audio track. This event can be used to modify stream config before playing a track. - * Listening to this event will pause the execution of audio player until `done()` is invoked. - * @param queue The queue where this event occurred - * @param track The track that will be streamed - * @param config Configurations for streaming - * @param done Done callback - */ - [GuildQueueEvent.WillPlayTrack]: (queue: GuildQueue, track: Track, config: StreamConfig, done: () => void) => unknown; - /** - * Emitted when a voice channel is populated - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.ChannelPopulate]: (queue: GuildQueue) => unknown; - /** - * Emitted when a queue is successfully created - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.QueueCreate]: (queue: GuildQueue) => unknown; - /** - * Emitted when a queue is successfully deleted - * @param queue The queue where this event occurred - */ - [GuildQueueEvent.QueueDelete]: (queue: GuildQueue) => unknown; - /** - * Emitted when a queue is trying to add similar track for autoplay - * @param queue The queue where this event occurred - * @param tracks The similar tracks that were found - * @param done Done callback - */ - [GuildQueueEvent.WillAutoPlay]: (queue: GuildQueue, tracks: Track[], done: (track: Track | null) => void) => unknown; -} - -export class GuildQueue { - #transitioning = false; - #deleted = false; - #shuffle = false; - private __current: Track | null = null; - public tracks: Queue; - public history = new GuildQueueHistory(this); - public dispatcher: StreamDispatcher | null = null; - public node = new GuildQueuePlayerNode(this); - public filters = new GuildQueueAudioFilters(this); - public onBeforeCreateStream: OnBeforeCreateStreamHandler = async () => null; - public onAfterCreateStream: OnAfterCreateStreamHandler = async (stream) => ({ - stream, - type: StreamType.Raw - }); - public repeatMode = QueueRepeatMode.OFF; - public timeouts = new Collection(); - public stats = new GuildQueueStatistics(this); - public tasksQueue = new AsyncQueue(); - public syncedLyricsProvider = new SyncedLyricsProvider(this); - - public constructor(public player: Player, public options: GuildNodeInit) { - this.tracks = new Queue(options.queueStrategy); - if (TypeUtil.isFunction(options.onBeforeCreateStream)) this.onBeforeCreateStream = options.onBeforeCreateStream; - if (TypeUtil.isFunction(options.onAfterCreateStream)) this.onAfterCreateStream = options.onAfterCreateStream; - if (!TypeUtil.isNullish(options.repeatMode)) this.repeatMode = options.repeatMode; - - options.selfDeaf ??= true; - options.maxSize ??= Infinity; - options.maxHistorySize ??= Infinity; - options.pauseOnEmpty ??= true; - - if (!TypeUtil.isNullish(this.options.biquad) && !TypeUtil.isBoolean(this.options.biquad)) { - this.filters._lastFiltersCache.biquad = this.options.biquad; - } - - if (Array.isArray(this.options.equalizer)) { - this.filters._lastFiltersCache.equalizer = this.options.equalizer; - } - - if (Array.isArray(this.options.filterer)) { - this.filters._lastFiltersCache.filters = this.options.filterer; - } - - if (TypeUtil.isNumber(this.options.resampler)) { - this.filters._lastFiltersCache.sampleRate = this.options.resampler; - } - - if (TypeUtil.isArray(this.options.ffmpegFilters)) { - this.filters.ffmpeg.setDefaults(this.options.ffmpegFilters); - } - - if (!TypeUtil.isNumber(options.maxSize)) { - throw Exceptions.ERR_INVALID_ARG_TYPE('[GuildNodeInit.maxSize]', 'number', typeof options.maxSize); - } - - if (!TypeUtil.isNumber(options.maxHistorySize)) { - throw Exceptions.ERR_INVALID_ARG_TYPE('[GuildNodeInit.maxHistorySize]', 'number', typeof options.maxHistorySize); - } - - if (options.maxSize < 1) options.maxSize = Infinity; - if (options.maxHistorySize < 1) options.maxHistorySize = Infinity; - - if (this.hasDebugger) this.debug(`GuildQueue initialized for guild ${this.options.guild.name} (ID: ${this.options.guild.id})`); - this.emit(GuildQueueEvent.queueCreate, this); - } - - /** - * Estimated duration of this queue in ms - */ - public get estimatedDuration() { - return this.tracks.store.reduce((a, c) => a + c.durationMS, 0); - } - - /** - * Formatted duration of this queue - */ - public get durationFormatted() { - return Util.buildTimeCode(Util.parseMS(this.estimatedDuration)); - } - - /** - * The voice receiver for this queue - */ - public get voiceReceiver() { - return this.dispatcher?.receiver ?? null; - } - - /** - * The sync lyrics provider for this queue. - * @example const lyrics = await player.lyrics.search({ q: 'Alan Walker Faded' }); - * const syncedLyrics = queue.syncedLyrics(lyrics[0]); - * console.log(syncedLyrics.at(10_000)); - * // subscribe to lyrics change - * const unsubscribe = syncedLyrics.onChange((lyrics, timestamp) => { - * console.log(lyrics, timestamp); - * }); - * // unsubscribe from lyrics change - * unsubscribe(); // or - * syncedLyrics.unsubscribe(); - */ - public syncedLyrics(lyrics: LrcGetResult | LrcSearchResult) { - this.syncedLyricsProvider.load(lyrics?.syncedLyrics ?? ''); - return this.syncedLyricsProvider; - } - - /** - * Write a debug message to this queue - * @param m The message to write - */ - public debug(m: string) { - this.emit(GuildQueueEvent.debug, this, m); - } - - /** - * The metadata of this queue - */ - public get metadata() { - return this.options.metadata!; - } - - public set metadata(m: Meta) { - this.options.metadata = m; - } - - /** - * Set metadata for this queue - * @param m Metadata to set - */ - public setMetadata(m: Meta) { - this.options.metadata = m; - } - - /** - * Indicates current track of this queue - */ - public get currentTrack() { - return this.dispatcher?.audioResource?.metadata || this.__current; - } - - /** - * Indicates if this queue was deleted previously - */ - public get deleted() { - return this.#deleted; - } - - /** - * The voice channel of this queue - */ - public get channel() { - return this.dispatcher?.channel || null; - } - - public set channel(c: VoiceBasedChannel | null) { - if (this.dispatcher) { - if (c) { - this.dispatcher.channel = c; - } else { - this.delete(); - } - } - } - - /** - * The voice connection of this queue - */ - public get connection() { - return this.dispatcher?.voiceConnection || null; - } - - /** - * The guild this queue belongs to - */ - public get guild() { - return this.options.guild; - } - - /** - * The id of this queue - */ - public get id() { - return this.guild.id; - } - - /** - * Set transition mode for this queue - * @param state The state to set - */ - public setTransitioning(state: boolean) { - this.#transitioning = state; - } - - /** - * if this queue is currently under transition mode - */ - public isTransitioning() { - return this.#transitioning; - } - - /** - * Set repeat mode for this queue - * @param mode The repeat mode to apply - */ - public setRepeatMode(mode: QueueRepeatMode) { - this.repeatMode = mode; - } - - /** - * Max size of this queue - */ - public get maxSize() { - return this.options.maxSize ?? Infinity; - } - - /** - * Max size of this queue - */ - public getMaxSize() { - return this.maxSize; - } - - /** - * Gets the size of the queue - */ - public get size() { - return this.tracks.size; - } - - /** - * The size of this queue - */ - public getSize() { - return this.size; - } - - /** - * Max history size of this queue - */ - public get maxHistorySize() { - return this.options.maxHistorySize ?? Infinity; - } - - /** - * Max history size of this queue - */ - public getMaxHistorySize() { - return this.maxHistorySize; - } - - /** - * Set max history size for this queue - * @param size The size to set - */ - public setMaxHistorySize(size: number) { - if (!TypeUtil.isNumber(size)) { - throw Exceptions.ERR_INVALID_ARG_TYPE('size', 'number', typeof size); - } - - if (size < 1) size = Infinity; - - this.options.maxHistorySize = size; - } - - /** - * Set max size for this queue - * @param size The size to set - */ - public setMaxSize(size: number) { - if (!TypeUtil.isNumber(size)) { - throw Exceptions.ERR_INVALID_ARG_TYPE('size', 'number', typeof size); - } - - if (size < 1) size = Infinity; - - this.options.maxSize = size; - } - - /** - * Clear this queue - */ - public clear() { - this.tracks.clear(); - this.history.clear(); - } - - /** - * Check if this queue has no tracks left in it - */ - public isEmpty() { - return this.tracks.size < 1; - } - - /** - * Check if this queue is full - */ - public isFull() { - return this.tracks.size >= this.maxSize; - } - - /** - * Get queue capacity - */ - public getCapacity() { - if (this.isFull()) return 0; - const cap = this.maxSize - this.size; - return cap; - } - - /** - * Check if this queue currently holds active audio resource - */ - public isPlaying() { - return this.dispatcher?.audioResource != null && !this.dispatcher.audioResource.ended; - } - - /** - * Add track to the queue. This will emit `audioTracksAdd` when multiple tracks are added, otherwise `audioTrackAdd`. - * @param track Track or playlist or array of tracks to add - */ - public addTrack(track: Track | Track[] | Playlist) { - const toAdd = track instanceof Playlist ? track.tracks : track; - const isMulti = Array.isArray(toAdd); - - VALIDATE_QUEUE_CAP(this, toAdd); - - this.tracks.add(toAdd); - - if (isMulti) { - this.emit(GuildQueueEvent.audioTracksAdd, this, toAdd); - } else { - this.emit(GuildQueueEvent.audioTrackAdd, this, toAdd); - } - } - - /** - * Remove a track from queue - * @param track The track to remove - */ - public removeTrack(track: TrackResolvable) { - return this.node.remove(track); - } - - /** - * Inserts the track to the given index - * @param track The track to insert - * @param index The index to insert the track at (defaults to 0) - */ - public insertTrack(track: Track, index = 0): void { - return this.node.insert(track, index); - } - - /** - * Moves a track in the queue - * @param from The track to move - * @param to The position to move to - */ - public moveTrack(track: TrackResolvable, index = 0): void { - return this.node.move(track, index); - } - - /** - * Copy a track in the queue - * @param from The track to clone - * @param to The position to clone at - */ - public copyTrack(track: TrackResolvable, index = 0): void { - return this.node.copy(track, index); - } - - /** - * Swap two tracks in the queue - * @param src The first track to swap - * @param dest The second track to swap - */ - public swapTracks(src: TrackResolvable, dest: TrackResolvable): void { - return this.node.swap(src, dest); - } - - /** - * Create stream dispatcher from the given connection - * @param connection The connection to use - */ - public createDispatcher(connection: VoiceConnection, options: Pick = {}) { - if (connection.state.status === VoiceConnectionStatus.Destroyed) { - throw Exceptions.ERR_VOICE_CONNECTION_DESTROYED(); - } - - const channel = this.player.client.channels.cache.get(connection.joinConfig.channelId!); - if (!channel) throw Exceptions.ERR_NO_VOICE_CHANNEL(); - if (!channel.isVoiceBased()) throw Exceptions.ERR_INVALID_ARG_TYPE('channel', `VoiceBasedChannel (type ${ChannelType.GuildVoice}/${ChannelType.GuildStageVoice})`, String(channel?.type)); - - if (this.dispatcher) { - this.#removeListeners(this.dispatcher); - this.dispatcher.destroy(); - this.dispatcher = null; - } - - this.dispatcher = new StreamDispatcher(connection, channel, this, options.timeout ?? this.options.connectionTimeout, options.audioPlayer); - } - - /** - * Connect to a voice channel - * @param channelResolvable The voice channel to connect to - * @param options Join config - */ - public async connect(channelResolvable: GuildVoiceChannelResolvable, options: VoiceConnectConfig = {}) { - const channel = this.player.client.channels.resolve(channelResolvable); - if (!channel || !channel.isVoiceBased()) { - throw Exceptions.ERR_INVALID_ARG_TYPE('channel', `VoiceBasedChannel (type ${ChannelType.GuildVoice}/${ChannelType.GuildStageVoice})`, String(channel?.type)); - } - - if (this.hasDebugger) this.debug(`Connecting to ${channel.type === ChannelType.GuildStageVoice ? 'stage' : 'voice'} channel ${channel.name} (ID: ${channel.id})`); - - if (this.dispatcher && channel.id !== this.dispatcher.channel.id) { - if (this.hasDebugger) this.debug('Destroying old connection'); - this.#removeListeners(this.dispatcher); - this.dispatcher.destroy(); - this.dispatcher = null; - } - - this.dispatcher = await this.player.voiceUtils.connect(channel, { - deaf: options.deaf ?? this.options.selfDeaf ?? true, - maxTime: options?.timeout ?? this.options.connectionTimeout ?? 120_000, - queue: this, - audioPlayer: options?.audioPlayer, - group: options.group ?? this.player.client.user?.id - }); - - this.emit(GuildQueueEvent.connection, this); - - if (this.channel!.type === ChannelType.GuildStageVoice) { - await this.channel!.guild.members.me!.voice.setSuppressed(false).catch(async () => { - return await this.channel!.guild.members.me!.voice.setRequestToSpeak(true).catch(Util.noop); - }); - } - - this.#attachListeners(this.dispatcher); - - return this; - } - - /** - * Enable shuffle mode for this queue - * @param dynamic Whether to shuffle the queue dynamically. Defaults to `true`. - * Dynamic shuffling will shuffle the queue when the current track ends, without mutating the queue. - * If set to `false`, the queue will be shuffled immediately in-place, which cannot be undone. - */ - public enableShuffle(dynamic = true) { - if (!dynamic) { - this.tracks.shuffle(); - return true; - } - - this.#shuffle = true; - return true; - } - - /** - * Disable shuffle mode for this queue. - */ - public disableShuffle() { - this.#shuffle = false; - return true; - } - - /** - * Toggle shuffle mode for this queue. - * @param dynamic Whether to shuffle the queue dynamically. Defaults to `true`. - * @returns Whether shuffle is enabled or disabled. - */ - public toggleShuffle(dynamic = true) { - if (dynamic) { - this.#shuffle = !this.#shuffle; - return this.#shuffle; - } else { - this.tracks.shuffle(); - return true; - } - } - - /** - * Whether shuffle mode is enabled for this queue. - */ - public get isShuffling() { - return this.#shuffle; - } - - /** - * The voice connection latency of this queue - */ - public get ping() { - return this.connection?.ping.udp ?? -1; - } - - /** - * Delete this queue - */ - public delete() { - if (this.player.nodes.delete(this.id)) { - this.#deleted = true; - this.player.events.emit(GuildQueueEvent.queueDelete, this); - this.node.tasksQueue.cancelAll(); - this.tasksQueue.cancelAll(); - } - } - - /** - * Revives this queue - * @returns - */ - public revive() { - if (!this.deleted || this.player.nodes.has(this.id)) return; - this.#deleted = false; - this.setTransitioning(false); - this.player.nodes.cache.set(this.id, this); - this.player.events.emit(GuildQueueEvent.queueCreate, this); - } - - /** - * Set self deaf - * @param mode On/Off state - * @param reason Reason - */ - public setSelfDeaf(mode?: boolean, reason?: string) { - return this.guild.members.me!.voice.setDeaf(mode, reason); - } - - /** - * Set self mute - * @param mode On/Off state - * @param reason Reason - */ - public setSelfMute(mode?: boolean, reason?: string) { - return this.guild.members.me!.voice.setMute(mode, reason); - } - - /** - * Play a track in this queue - * @param track The track to be played - * @param options Player node initialization options - */ - public async play(track: TrackLike, options?: PlayerNodeInitializerOptions) { - if (!this.channel) throw Exceptions.ERR_NO_VOICE_CONNECTION(); - - return this.player.play(this.channel, track, options); - } - - /** - * Emit an event on this queue - * @param event The event to emit - * @param args The args for the event - */ - public emit>(event: K, ...args: Parameters[K]>): boolean { - if (this.deleted) return false; - return this.player.events.emit(event, ...args); - } - - #attachListeners(dispatcher: StreamDispatcher) { - dispatcher.on('error', (e) => this.emit(GuildQueueEvent.error, this, e)); - dispatcher.on('debug', (m) => this.hasDebugger && this.emit(GuildQueueEvent.debug, this, m)); - dispatcher.on('finish', (r) => this.#performFinish(r)); - dispatcher.on('start', (r) => this.#performStart(r)); - dispatcher.on('destroyed', () => { - this.#removeListeners(dispatcher); - this.dispatcher = null; - }); - dispatcher.on('dsp', (f) => { - if (!Object.is(this.filters._lastFiltersCache.filters, f)) { - this.emit(GuildQueueEvent.dspUpdate, this, this.filters._lastFiltersCache.filters, f); - } - this.filters._lastFiltersCache.filters = f; - }); - dispatcher.on('biquad', (f) => { - if (this.filters._lastFiltersCache.biquad !== f) { - this.emit(GuildQueueEvent.biquadFiltersUpdate, this, this.filters._lastFiltersCache.biquad, f); - } - this.filters._lastFiltersCache.biquad = f; - }); - dispatcher.on('eqBands', (f) => { - if (!Object.is(f, this.filters._lastFiltersCache.equalizer)) { - this.emit(GuildQueueEvent.equalizerUpdate, this, this.filters._lastFiltersCache.equalizer, f); - } - this.filters._lastFiltersCache.equalizer = f; - }); - dispatcher.on('volume', (f) => { - if (this.filters._lastFiltersCache.volume !== f) this.emit(GuildQueueEvent.volumeChange, this, this.filters._lastFiltersCache.volume, f); - this.filters._lastFiltersCache.volume = f; - }); - } - - public get hasDebugger() { - return this.player.events.hasDebugger; - } - - #removeListeners unknown }>(target: T) { - target.removeAllListeners(); - } - - #performStart(resource?: AudioResource) { - const track = resource?.metadata || this.currentTrack; - const reason = this.isTransitioning() ? 'filters' : 'normal'; - - if (this.hasDebugger) - this.debug( - `Player triggered for Track ${JSON.stringify({ - title: track?.title, - reason - })}` - ); - - this.emit(GuildQueueEvent.playerTrigger, this, track!, reason); - if (track && !this.isTransitioning()) this.emit(GuildQueueEvent.playerStart, this, track); - this.setTransitioning(false); - } - - #getNextTrack() { - if (!this.isShuffling) { - return this.tracks.dispatch(); - } - - const store = this.tracks.store; - - if (!store.length) return; - - const track = Util.randomChoice(store); - - this.tracks.removeOne((t) => { - return t.id === track.id; - }); - - return track; - } - - #performFinish(resource?: AudioResource) { - const track = resource?.metadata || this.currentTrack; - - if (this.hasDebugger) - this.debug( - `Track ${JSON.stringify({ - title: track?.title, - isTransitionMode: this.isTransitioning() - })} was marked as finished` - ); - - if (track && !this.isTransitioning()) { - this.syncedLyricsProvider.unsubscribe(); - this.syncedLyricsProvider.lyrics.clear(); - if (this.hasDebugger) this.debug('Adding track to history and emitting finish event since transition mode is disabled...'); - this.history.push(track); - this.node.resetProgress(); - this.emit(GuildQueueEvent.playerFinish, this, track); - if (this.#deleted) return this.#emitEnd(); - if (this.tracks.size < 1 && this.repeatMode === QueueRepeatMode.OFF) { - if (this.hasDebugger) this.debug('No more tracks left in the queue to play and repeat mode is off, initiating #emitEnd()'); - this.#emitEnd(); - } else { - if (this.repeatMode === QueueRepeatMode.TRACK) { - if (this.hasDebugger) this.debug('Repeat mode is set to track, repeating last track from the history...'); - this.__current = this.history.tracks.dispatch() || track; - return this.node.play(this.__current!, { queue: false }); - } - if (this.repeatMode === QueueRepeatMode.QUEUE) { - if (this.hasDebugger) this.debug('Repeat mode is set to queue, moving last track from the history to current queue...'); - this.tracks.add(this.history.tracks.dispatch() || track); - } - if (!this.tracks.size) { - if (this.repeatMode === QueueRepeatMode.AUTOPLAY) { - if (this.hasDebugger) this.debug('Repeat mode is set to autoplay, initiating autoplay handler...'); - this.#handleAutoplay(track); - return; - } - } else { - if (this.hasDebugger) this.debug('Initializing next track of the queue...'); - this.__current = this.#getNextTrack()!; - this.node.play(this.__current, { - queue: false - }); - } - } - } - } - - #emitEnd() { - this.__current = null; - this.emit(GuildQueueEvent.emptyQueue, this); - if (this.options.leaveOnEnd) { - const tm: NodeJS.Timeout = setTimeout(() => { - if (this.isPlaying()) return clearTimeout(tm); - this.dispatcher?.disconnect(); - }, this.options.leaveOnEndCooldown).unref(); - } - } - - async #handleAutoplay(track: Track) { - try { - if (this.hasDebugger) this.debug(`Autoplay >> Finding related tracks for Track ${track.title} (${track.url}) [ext:${track.extractor?.identifier || 'N/A'}]`); - const tracks = - (await track.extractor?.getRelatedTracks(track, this.history))?.tracks || - ( - await this.player.extractors.run(async (ext) => { - if (this.hasDebugger) this.debug(`Autoplay >> Querying extractor ${ext.identifier}`); - const res = await ext.getRelatedTracks(track, this.history); - if (!res.tracks.length) { - if (this.hasDebugger) this.debug(`Autoplay >> Extractor ${ext.identifier} failed to provide results.`); - return false; - } - - if (this.hasDebugger) this.debug(`Autoplay >> Extractor ${ext.identifier} successfully returned results.`); - - return res.tracks; - }) - )?.result || - []; - - let resolver: (track: Track | null) => void = Util.noop; - const donePromise = new Promise((resolve) => (resolver = resolve)); - - const success = this.emit(GuildQueueEvent.willAutoPlay, this, tracks, resolver!); - - // prevent dangling promise - if (!success) { - resolver( - tracks.length - ? (() => { - const unique = tracks.filter((tr) => !this.history.tracks.find((t) => t.url === tr.url)); - return unique?.[0] ?? Util.randomChoice(tracks.slice(0, 5)); - })() - : null - ); - } - - const nextTrack = await donePromise; - - if (!nextTrack) { - if (this.hasDebugger) this.debug('Autoplay >> No track was found, initiating #emitEnd()'); - throw 'No track was found'; - } - - await this.node.play(nextTrack, { - queue: false, - seek: 0, - transitionMode: false - }); - } catch { - return this.#emitEnd(); - } - } -} diff --git a/packages/discord-player/src/queue/GuildQueueAudioFilters.ts b/packages/discord-player/src/queue/GuildQueueAudioFilters.ts deleted file mode 100644 index cd263cde44..0000000000 --- a/packages/discord-player/src/queue/GuildQueueAudioFilters.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { Readable } from 'stream'; -import { FiltersName, QueueFilters } from '../types/types'; -import { AudioFilters } from '../utils/AudioFilters'; -import { GuildQueue, GuildQueueEvent } from './GuildQueue'; -import { BiquadFilters, Equalizer, EqualizerBand, PCMFilters } from '@discord-player/equalizer'; -import { FFmpegStreamOptions, createFFmpegStream } from '../utils/FFmpegStream'; -import { Exceptions } from '../errors'; - -type Filters = keyof typeof AudioFilters.filters; - -const makeBands = (arr: number[]) => { - return Array.from( - { - length: Equalizer.BAND_COUNT - }, - (_, i) => ({ - band: i, - gain: arr[i] ? arr[i] / 30 : 0 - }) - ) as EqualizerBand[]; -}; - -type EQPreset = { - Flat: EqualizerBand[]; - Classical: EqualizerBand[]; - Club: EqualizerBand[]; - Dance: EqualizerBand[]; - FullBass: EqualizerBand[]; - FullBassTreble: EqualizerBand[]; - FullTreble: EqualizerBand[]; - Headphones: EqualizerBand[]; - LargeHall: EqualizerBand[]; - Live: EqualizerBand[]; - Party: EqualizerBand[]; - Pop: EqualizerBand[]; - Reggae: EqualizerBand[]; - Rock: EqualizerBand[]; - Ska: EqualizerBand[]; - Soft: EqualizerBand[]; - SoftRock: EqualizerBand[]; - Techno: EqualizerBand[]; -}; - -export const EqualizerConfigurationPreset: Readonly = Object.freeze({ - Flat: makeBands([]), - Classical: makeBands([-1.11022e-15, -1.11022e-15, -1.11022e-15, -1.11022e-15, -1.11022e-15, -1.11022e-15, -7.2, -7.2, -7.2, -9.6]), - Club: makeBands([-1.11022e-15, -1.11022e-15, 8.0, 5.6, 5.6, 5.6, 3.2, -1.11022e-15, -1.11022e-15, -1.11022e-15]), - Dance: makeBands([9.6, 7.2, 2.4, -1.11022e-15, -1.11022e-15, -5.6, -7.2, -7.2, -1.11022e-15, -1.11022e-15]), - FullBass: makeBands([-8.0, 9.6, 9.6, 5.6, 1.6, -4.0, -8.0, -10.4, -11.2, -11.2]), - FullBassTreble: makeBands([7.2, 5.6, -1.11022e-15, -7.2, -4.8, 1.6, 8.0, 11.2, 12.0, 12.0]), - FullTreble: makeBands([-9.6, -9.6, -9.6, -4.0, 2.4, 11.2, 16.0, 16.0, 16.0, 16.8]), - Headphones: makeBands([4.8, 11.2, 5.6, -3.2, -2.4, 1.6, 4.8, 9.6, 12.8, 14.4]), - LargeHall: makeBands([10.4, 10.4, 5.6, 5.6, -1.11022e-15, -4.8, -4.8, -4.8, -1.11022e-15, -1.11022e-15]), - Live: makeBands([-4.8, -1.11022e-15, 4.0, 5.6, 5.6, 5.6, 4.0, 2.4, 2.4, 2.4]), - Party: makeBands([7.2, 7.2, -1.11022e-15, -1.11022e-15, -1.11022e-15, -1.11022e-15, -1.11022e-15, -1.11022e-15, 7.2, 7.2]), - Pop: makeBands([-1.6, 4.8, 7.2, 8.0, 5.6, -1.11022e-15, -2.4, -2.4, -1.6, -1.6]), - Reggae: makeBands([-1.11022e-15, -1.11022e-15, -1.11022e-15, -5.6, -1.11022e-15, 6.4, 6.4, -1.11022e-15, -1.11022e-15, -1.11022e-15]), - Rock: makeBands([8.0, 4.8, -5.6, -8.0, -3.2, 4.0, 8.8, 11.2, 11.2, 11.2]), - Ska: makeBands([-2.4, -4.8, -4.0, -1.11022e-15, 4.0, 5.6, 8.8, 9.6, 11.2, 9.6]), - Soft: makeBands([4.8, 1.6, -1.11022e-15, -2.4, -1.11022e-15, 4.0, 8.0, 9.6, 11.2, 12.0]), - SoftRock: makeBands([4.0, 4.0, 2.4, -1.11022e-15, -4.0, -5.6, -3.2, -1.11022e-15, 2.4, 8.8]), - Techno: makeBands([8.0, 5.6, -1.11022e-15, -5.6, -4.8, -1.11022e-15, 8.0, 9.6, 9.6, 8.8]) -}); - -export class FFmpegFilterer { - #ffmpegFilters: Filters[] = []; - #inputArgs: string[] = []; - public constructor(public af: GuildQueueAudioFilters) {} - - /** - * Indicates whether ffmpeg may be skipped - */ - public get skippable() { - return !!this.af.queue.player.options.skipFFmpeg; - } - - #setFilters(filters: Filters[]) { - const { queue } = this.af; - // skip if filters are the same - if (filters.every((f) => this.#ffmpegFilters.includes(f)) && this.#ffmpegFilters.every((f) => filters.includes(f))) return Promise.resolve(false); - const ignoreFilters = this.filters.some((ff) => ff === 'nightcore' || ff === 'vaporwave') && !filters.some((ff) => ff === 'nightcore' || ff === 'vaporwave'); - const seekTime = queue.node.getTimestamp(ignoreFilters)?.current.value || 0; - const prev = this.#ffmpegFilters.slice(); - this.#ffmpegFilters = [...new Set(filters)]; - - return this.af.triggerReplay(seekTime).then((t) => { - queue.emit(GuildQueueEvent.audioFiltersUpdate, queue, prev, this.#ffmpegFilters.slice()); - return t; - }); - } - - /** - * Set input args for FFmpeg - */ - public setInputArgs(args: string[]) { - if (!args.every((arg) => typeof arg === 'string')) throw Exceptions.ERR_INVALID_ARG_TYPE('args', 'Array', 'invalid item(s)'); - this.#inputArgs = args; - } - - /** - * Get input args - */ - public get inputArgs() { - return this.#inputArgs; - } - - /** - * Get encoder args - */ - public get encoderArgs() { - if (!this.filters.length) return []; - - return ['-af', this.toString()]; - } - - /** - * Get final ffmpeg args - */ - public get args() { - return this.inputArgs.concat(this.encoderArgs); - } - - /** - * Create ffmpeg stream - * @param source The stream source - * @param options The stream options - */ - public createStream(source: string | Readable, options: FFmpegStreamOptions) { - if (this.#inputArgs.length) options.encoderArgs = [...this.#inputArgs, ...(options.encoderArgs || [])]; - return createFFmpegStream(source, options); - } - - /** - * Set ffmpeg filters - * @param filters The filters - */ - public setFilters(filters: Filters[] | Record | string[] | boolean) { - let _filters: Filters[] = []; - if (typeof filters === 'boolean') { - _filters = !filters ? [] : (Object.keys(AudioFilters.filters) as Filters[]); - } else if (Array.isArray(filters)) { - _filters = filters as Filters[]; - } else { - _filters = Object.entries(filters) - .filter((res) => res[1] === true) - .map((m) => m[0]) as Filters[]; - } - - return this.#setFilters(_filters); - } - - /** - * Currently active ffmpeg filters - */ - public get filters() { - return this.#ffmpegFilters; - } - - public set filters(filters: Filters[]) { - this.setFilters(filters); - } - - /** - * Toggle given ffmpeg filter(s) - * @param filters The filter(s) - */ - public toggle(filters: Filters[] | Filters) { - if (!Array.isArray(filters)) filters = [filters]; - const fresh: Filters[] = []; - - filters.forEach((f) => { - if (this.filters.includes(f)) return; - fresh.push(f); - }); - - return this.#setFilters(this.#ffmpegFilters.filter((r) => !filters.includes(r)).concat(fresh)); - } - - /** - * Set default filters - * @param ff Filters list - */ - public setDefaults(ff: Filters[]) { - this.#ffmpegFilters = ff; - } - - /** - * Get list of enabled filters - */ - public getFiltersEnabled() { - return this.#ffmpegFilters; - } - - /** - * Get list of disabled filters - */ - public getFiltersDisabled() { - return AudioFilters.names.filter((f) => !this.#ffmpegFilters.includes(f)); - } - - /** - * Check if the given filter is enabled - * @param filter The filter - */ - public isEnabled(filter: T): boolean { - return this.#ffmpegFilters.includes(filter); - } - - /** - * Check if the given filter is disabled - * @param filter The filter - */ - public isDisabled(filter: T): boolean { - return !this.isEnabled(filter); - } - - /** - * Check if the given filter is a valid filter - * @param filter The filter to test - */ - public isValidFilter(filter: string): filter is FiltersName { - return AudioFilters.has(filter as Filters); - } - - /** - * Convert current filters to array - */ - public toArray() { - return this.filters.map((filter) => AudioFilters.get(filter)); - } - - /** - * Convert current filters to JSON object - */ - public toJSON() { - const obj = {} as Record; - - this.filters.forEach((filter) => (obj[filter] = AudioFilters.get(filter))); - - return obj; - } - - /** - * String representation of current filters - */ - public toString() { - return AudioFilters.create(this.filters); - } -} - -export interface GuildQueueAFiltersCache { - equalizer: EqualizerBand[]; - biquad: BiquadFilters | null; - filters: PCMFilters[]; - volume: number; - sampleRate: number; -} - -export class GuildQueueAudioFilters { - public graph = new AFilterGraph(this); - public ffmpeg = new FFmpegFilterer(this); - public equalizerPresets = EqualizerConfigurationPreset; - public _lastFiltersCache: GuildQueueAFiltersCache = { - biquad: null, - equalizer: [], - filters: [], - volume: 100, - sampleRate: -1 - }; - public constructor(public queue: GuildQueue) { - if (typeof this.queue.options.volume === 'number') { - this._lastFiltersCache.volume = this.queue.options.volume; - } - } - - // TODO: enable this in the future - // public get ffmpeg(): FFmpegFilterer | null { - // if (this.queue.player.options.skipFFmpeg) { - // if (this.#ffmpeg) this.#ffmpeg = null; - // return null; - // } - - // if (!this.#ffmpeg) { - // this.#ffmpeg = new FFmpegFilterer(this); - // } - - // return this.#ffmpeg; - // } - - /** - * Volume transformer - */ - public get volume() { - return this.queue.dispatcher?.dsp?.volume || null; - } - - /** - * 15 Band Equalizer - */ - public get equalizer() { - return this.queue.dispatcher?.equalizer || null; - } - - /** - * Digital biquad filters - */ - public get biquad() { - return this.queue.dispatcher?.biquad || null; - } - - /** - * DSP filters - */ - public get filters() { - return this.queue.dispatcher?.filters || null; - } - - /** - * Audio resampler - */ - public get resampler() { - return this.queue.dispatcher?.resampler || null; - } - - /** - * Replay current track in transition mode - * @param seek The duration to seek to - */ - public async triggerReplay(seek = 0) { - if (!this.queue.currentTrack) return false; - const entry = this.queue.node.tasksQueue.acquire(); - try { - await entry.getTask(); - await this.queue.node.play(this.queue.currentTrack, { - queue: false, - seek, - transitionMode: true - }); - this.queue.node.tasksQueue.release(); - return true; - } catch { - this.queue.node.tasksQueue.release(); - return false; - } - } -} - -export class AFilterGraph { - public constructor(public af: GuildQueueAudioFilters) {} - - public get ffmpeg() { - return this.af.ffmpeg?.filters ?? []; - } - - public get equalizer() { - return (this.af.equalizer?.bandMultipliers || []).map((m, i) => ({ - band: i, - gain: m - })) as EqualizerBand[]; - } - - public get biquad() { - return (this.af.biquad?.getFilterName() as Exclude | null) || null; - } - - public get filters() { - return this.af.filters?.filters || []; - } - - public get volume() { - return this.af.volume; - } - - public get resampler() { - return this.af.resampler; - } - - public dump(): FilterGraph { - return { - ffmpeg: this.ffmpeg, - equalizer: this.equalizer, - biquad: this.biquad, - filters: this.filters, - sampleRate: this.resampler?.targetSampleRate || this.resampler?.sampleRate || 48000, - volume: this.volume?.volume ?? 100 - }; - } -} - -export interface FilterGraph { - ffmpeg: Filters[]; - equalizer: EqualizerBand[]; - biquad: Exclude | null; - filters: PCMFilters[]; - volume: number; - sampleRate: number; -} diff --git a/packages/discord-player/src/queue/GuildQueueHistory.ts b/packages/discord-player/src/queue/GuildQueueHistory.ts deleted file mode 100644 index ffba57c1ec..0000000000 --- a/packages/discord-player/src/queue/GuildQueueHistory.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Queue } from '@discord-player/utils'; -import { Exceptions } from '../errors'; -import { Track } from '../fabric/Track'; -import { GuildQueue, TrackSkipReason } from './GuildQueue'; - -export class GuildQueueHistory { - public tracks = new Queue('LIFO'); - public constructor(public queue: GuildQueue) {} - - /** - * Current track in the queue - */ - public get currentTrack() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return this.queue.dispatcher?.audioResource?.metadata || ((this.queue as any).__current as Track | null); - } - - /** - * Next track in the queue - */ - public get nextTrack() { - return this.queue.tracks.at(0) || null; - } - - /** - * Previous track in the queue - */ - public get previousTrack() { - return this.tracks.at(0) || null; - } - - /** - * If history is disabled - */ - public get disabled() { - return this.queue.options.disableHistory; - } - - /** - * Gets the size of the queue - */ - public get size() { - return this.tracks.size; - } - - public getSize() { - return this.size; - } - - /** - * If history is empty - */ - public isEmpty() { - return this.tracks.size < 1; - } - - /** - * Add track to track history - * @param track The track to add - */ - public push(track: Track | Track[]) { - if (this.disabled) return false; - this.tracks.add(track); - - this.resize(); - - return true; - } - - /** - * Clear history - */ - public clear() { - this.tracks.clear(); - } - - /** - * Play the next track in the queue - */ - public async next() { - const track = this.nextTrack; - if (!track) { - throw Exceptions.ERR_NO_RESULT('No next track in the queue'); - } - - this.queue.node.skip({ - reason: TrackSkipReason.HistoryNext, - description: 'Skipped by GuildQueueHistory.next()' - }); - } - - /** - * Play the previous track in the queue - */ - public async previous(preserveCurrent = true) { - const track = this.tracks.dispatch(); - if (!track) { - throw Exceptions.ERR_NO_RESULT('No previous track in the queue'); - } - - const current = this.currentTrack; - - await this.queue.node.play(track, { queue: false }); - if (current && preserveCurrent) this.queue.node.insert(current, 0); - } - - /** - * Alias to [GuildQueueHistory].previous() - */ - public back(preserveCurrent = true) { - return this.previous(preserveCurrent); - } - - /** - * Resize history store - */ - public resize() { - if (!Number.isFinite(this.queue.maxHistorySize)) return; - if (this.tracks.store.length < this.queue.maxHistorySize) return; - this.tracks.store.splice(this.queue.maxHistorySize); - } -} diff --git a/packages/discord-player/src/queue/GuildQueuePlayerNode.ts b/packages/discord-player/src/queue/GuildQueuePlayerNode.ts deleted file mode 100644 index 7ae5906d1d..0000000000 --- a/packages/discord-player/src/queue/GuildQueuePlayerNode.ts +++ /dev/null @@ -1,733 +0,0 @@ -import { AudioResource, StreamType } from 'discord-voip'; -import { Readable } from 'stream'; -import { PlayerProgressbarOptions, SearchQueryType } from '../types/types'; -import { QueryResolver } from '../utils/QueryResolver'; -import { Util, VALIDATE_QUEUE_CAP } from '../utils/Util'; -import { Track, TrackResolvable } from '../fabric/Track'; -import { GuildQueue, GuildQueueEvent, TrackSkipReason } from './GuildQueue'; -import { setTimeout as waitFor } from 'timers/promises'; -import { AsyncQueue } from '../utils/AsyncQueue'; -import { Exceptions } from '../errors'; -import { TypeUtil } from '../utils/TypeUtil'; -import { CreateStreamOps } from '../VoiceInterface/StreamDispatcher'; -import { ExtractorStreamable } from '../extractors/BaseExtractor'; -import * as prism from 'prism-media'; -import { OpusDecoder } from '@discord-player/opus'; - -export const FFMPEG_SRATE_REGEX = /asetrate=\d+\*(\d(\.\d)?)/; - -export interface ResourcePlayOptions { - queue?: boolean; - seek?: number; - transitionMode?: boolean; -} - -export interface SkipOptions { - reason: TrackSkipReason; - description: string; -} - -export interface PlayerTimestamp { - current: { - label: string; - value: number; - }; - total: { - label: string; - value: number; - }; - progress: number; -} - -export interface StreamConfig { - dispatcherConfig: CreateStreamOps; - playerConfig: ResourcePlayOptions; -} - -export class GuildQueuePlayerNode { - #progress = 0; - #hasFFmpegOptimization = false; - public tasksQueue = new AsyncQueue(); - public constructor(public queue: GuildQueue) { - this.#hasFFmpegOptimization = /libopus: (yes|true)/.test(this.queue.player.scanDeps()); - } - - /** - * If the player is currently in idle mode - */ - public isIdle() { - return !!this.queue.dispatcher?.isIdle(); - } - - /** - * If the player is currently buffering the track - */ - public isBuffering() { - return !!this.queue.dispatcher?.isBuffering(); - } - - /** - * If the player is currently playing a track - */ - public isPlaying() { - return !!this.queue.dispatcher?.isPlaying(); - } - - /** - * If the player is currently paused - */ - public isPaused() { - return !!this.queue.dispatcher?.isPaused(); - } - - /** - * Reset progress history - */ - public resetProgress() { - this.#progress = 0; - } - - /** - * Set player progress - */ - public setProgress(progress: number) { - this.#progress = progress; - } - - /** - * The stream time for current session - */ - public get streamTime() { - return this.queue.dispatcher?.streamTime ?? 0; - } - - /** - * Current playback duration with history included - */ - public get playbackTime() { - const dur = this.#progress + this.streamTime; - - return dur; - } - - /** - * Get duration multiplier - */ - public getDurationMultiplier() { - const srateFilters = this.queue.filters.ffmpeg.toArray().filter((ff) => FFMPEG_SRATE_REGEX.test(ff)); - const multipliers = srateFilters - .map((m) => { - return parseFloat(FFMPEG_SRATE_REGEX.exec(m)?.[1] as string); - }) - .filter((f) => !isNaN(f)); - - return !multipliers.length ? 1 : multipliers.reduce((accumulator, current) => current + accumulator); - } - - /** - * Estimated progress of the player - */ - public get estimatedPlaybackTime() { - const dur = this.playbackTime; - return Math.round(this.getDurationMultiplier() * dur); - } - - /** - * Estimated total duration of the player - */ - public get estimatedDuration() { - const dur = this.totalDuration; - - return Math.round(dur / this.getDurationMultiplier()); - } - - /** - * Total duration of the current audio track - */ - public get totalDuration() { - const prefersBridgedMetadata = this.queue.options.preferBridgedMetadata; - const track = this.queue.currentTrack; - - if (prefersBridgedMetadata && track?.metadata != null && typeof track.metadata === 'object' && 'bridge' in track.metadata) { - const duration = ( - track as Track<{ - bridge: { - duration: number; - }; - }> - ).metadata?.bridge.duration; - - if (TypeUtil.isNumber(duration)) return duration; - } - - return track?.durationMS ?? 0; - } - - /** - * Get stream progress - * @param ignoreFilters Ignore filters - */ - public getTimestamp(ignoreFilters = false): PlayerTimestamp | null { - if (!this.queue.currentTrack) return null; - - const current = ignoreFilters ? this.playbackTime : this.estimatedPlaybackTime; - const total = ignoreFilters ? this.totalDuration : this.estimatedDuration; - - return { - current: { - label: Util.buildTimeCode(Util.parseMS(current)), - value: current - }, - total: { - label: Util.buildTimeCode(Util.parseMS(total)), - value: total - }, - progress: Math.round((current / total) * 100) - }; - } - - /** - * Create progress bar for current progress - * @param options Progress bar options - */ - public createProgressBar(options?: PlayerProgressbarOptions) { - const timestamp = this.getTimestamp(); - if (!timestamp) return null; - const { indicator = '\u{1F518}', leftChar = '\u25AC', rightChar = '\u25AC', length = 15, timecodes = true, separator = '\u2503' } = options || {}; - if (isNaN(length) || length < 0 || !Number.isFinite(length)) { - throw Exceptions.ERR_OUT_OF_RANGE('[PlayerProgressBarOptions.length]', String(length), '0', 'Finite Number'); - } - const index = Math.round((timestamp.current.value / timestamp.total.value) * length); - if (index >= 1 && index <= length) { - const bar = leftChar.repeat(index - 1).split(''); - bar.push(indicator); - bar.push(rightChar.repeat(length - index)); - if (timecodes) { - return `${timestamp.current.label} ${separator} ${bar.join('')} ${separator} ${timestamp.total.label}`; - } else { - return `${bar.join('')}`; - } - } else { - if (timecodes) { - return `${timestamp.current.label} ${separator} ${indicator}${rightChar.repeat(length - 1)} ${separator} ${timestamp.total.label}`; - } else { - return `${indicator}${rightChar.repeat(length - 1)}`; - } - } - } - - /** - * Seek the player - * @param duration The duration to seek to - */ - public async seek(duration: number) { - if (!this.queue.currentTrack) return false; - if (duration === this.estimatedPlaybackTime) return true; - if (duration > this.totalDuration) { - return this.skip({ - reason: TrackSkipReason.SEEK_OVER_THRESHOLD, - description: Exceptions.ERR_OUT_OF_RANGE('[duration]', String(duration), '0', String(this.totalDuration)).message - }); - } - if (duration < 0) duration = 0; - return await this.queue.filters.triggerReplay(duration); - } - - /** - * Current volume - */ - public get volume() { - return this.queue.dispatcher?.volume ?? 100; - } - - /** - * Set volume - * @param vol Volume amount to set - */ - public setVolume(vol: number) { - if (!this.queue.dispatcher) return false; - const res = this.queue.dispatcher.setVolume(vol); - if (res) this.queue.filters._lastFiltersCache.volume = vol; - return res; - } - - /** - * Set bit rate - * @param rate The bit rate to set - */ - public setBitrate(rate: number | 'auto') { - this.queue.dispatcher?.audioResource?.encoder?.setBitrate(rate === 'auto' ? this.queue.channel?.bitrate ?? 64000 : rate); - } - - /** - * Set paused state - * @param state The state - */ - public setPaused(state: boolean) { - if (state) return this.queue.dispatcher?.pause(true) || false; - return this.queue.dispatcher?.resume() || false; - } - - /** - * Pause the playback - */ - public pause() { - return this.setPaused(true); - } - - /** - * Resume the playback - */ - public resume() { - return this.setPaused(false); - } - - /** - * Skip current track - */ - public skip(options?: SkipOptions) { - if (!this.queue.dispatcher) return false; - const track = this.queue.currentTrack; - if (!track) return false; - this.queue.setTransitioning(false); - this.queue.dispatcher.end(); - const { reason, description } = options || { - reason: TrackSkipReason.Manual, - description: 'The track was skipped manually' - }; - this.queue.emit(GuildQueueEvent.playerSkip, this.queue, track, reason, description); - return true; - } - - /** - * Remove the given track from queue - * @param track The track to remove - * @param emitEvent Whether or not to emit the event @defaultValue true - */ - public remove(track: TrackResolvable, emitEvent = true) { - const foundTrack = this.queue.tracks.find((t, idx) => { - if (track instanceof Track || typeof track === 'string') { - return (typeof track === 'string' ? track : track.id) === t.id; - } - if (typeof track === 'string') return track === t.id; - return idx === track; - }); - if (!foundTrack) return null; - - this.queue.tracks.removeOne((t) => t.id === foundTrack.id); - - if (emitEvent) this.queue.emit(GuildQueueEvent.audioTrackRemove, this.queue, foundTrack); - - return foundTrack; - } - - /** - * Jump to specific track on the queue - * @param track The track to jump to without removing other tracks - */ - public jump(track: TrackResolvable) { - const removed = this.remove(track, false); - if (!removed) return false; - this.queue.tracks.store.unshift(removed); - return this.skip({ - reason: TrackSkipReason.Jump, - description: 'The track was jumped to manually' - }); - } - - /** - * Get track position - * @param track The track - */ - public getTrackPosition(track: TrackResolvable): number { - return this.queue.tracks.toArray().findIndex((t, idx) => { - if (track instanceof Track || typeof track === 'string') { - return (typeof track === 'string' ? track : track.id) === t.id; - } - if (typeof track === 'string') return track === t.id; - return idx === track; - }); - } - - /** - * Skip to the given track, removing others on the way - * @param track The track to skip to - */ - public skipTo(track: TrackResolvable) { - const idx = this.getTrackPosition(track); - if (idx < 0) return false; - const removed = this.remove(idx); - if (!removed) return false; - const toRemove = this.queue.tracks.store.filter((_, i) => i <= idx); - this.queue.tracks.store.splice(0, idx, removed); - this.queue.emit(GuildQueueEvent.audioTracksRemove, this.queue, toRemove); - return this.skip({ - reason: TrackSkipReason.SkipTo, - description: 'The player was skipped to another track manually' - }); - } - - /** - * Insert a track on the given position in queue - * @param track The track to insert - * @param index The position to insert to, defaults to 0. - */ - public insert(track: Track, index = 0) { - if (!(track instanceof Track)) throw Exceptions.ERR_INVALID_ARG_TYPE('track value', 'instance of Track', String(track)); - VALIDATE_QUEUE_CAP(this.queue, track); - this.queue.tracks.store.splice(index, 0, track); - if (!this.queue.options.noEmitInsert) this.queue.emit(GuildQueueEvent.audioTrackAdd, this.queue, track); - } - - /** - * Moves a track in the queue - * @param from The track to move - * @param to The position to move to - */ - public move(from: TrackResolvable, to: number) { - const removed = this.remove(from); - if (!removed) { - throw Exceptions.ERR_NO_RESULT('invalid track to move'); - } - this.insert(removed, to); - } - - /** - * Copy a track in the queue - * @param from The track to clone - * @param to The position to clone at - */ - public copy(from: TrackResolvable, to: number) { - const src = this.queue.tracks.at(this.getTrackPosition(from)); - if (!src) { - throw Exceptions.ERR_NO_RESULT('invalid track to copy'); - } - this.insert(src, to); - } - - /** - * Swap two tracks in the queue - * @param first The first track to swap - * @param second The second track to swap - */ - public swap(first: TrackResolvable, second: TrackResolvable) { - const src = this.getTrackPosition(first); - if (src < 0) throw Exceptions.ERR_NO_RESULT('invalid src track to swap'); - - const dest = this.getTrackPosition(second); - if (dest < 0) throw Exceptions.ERR_NO_RESULT('invalid dest track to swap'); - - const srcT = this.queue.tracks.store[src]; - const destT = this.queue.tracks.store[dest]; - - this.queue.tracks.store[src] = destT; - this.queue.tracks.store[dest] = srcT; - } - - /** - * Stop the playback - * @param force Whether or not to forcefully stop the playback - */ - public stop(force = false) { - this.queue.tracks.clear(); - this.queue.history.clear(); - if (!this.queue.dispatcher) return false; - this.queue.dispatcher.end(); - if (force) { - this.queue.dispatcher.destroy(); - return true; - } - if (this.queue.options.leaveOnStop) { - const tm: NodeJS.Timeout = setTimeout(() => { - if (this.isPlaying() || this.queue.tracks.size) return clearTimeout(tm); - this.queue.dispatcher?.destroy(); - }, this.queue.options.leaveOnStopCooldown).unref(); - } - return true; - } - - /** - * Play raw audio resource - * @param resource The audio resource to play - */ - public async playRaw(resource: AudioResource) { - await this.queue.dispatcher?.playStream(resource as AudioResource); - } - - /** - * Play the given track - * @param res The track to play - * @param options Options for playing the track - */ - public async play(res?: Track | null, options?: ResourcePlayOptions) { - if (!this.queue.dispatcher?.voiceConnection) { - throw Exceptions.ERR_NO_VOICE_CONNECTION(); - } - - if (this.queue.hasDebugger) this.queue.debug(`Received play request from guild ${this.queue.guild.name} (ID: ${this.queue.guild.id})`); - - options = Object.assign( - {}, - { - queue: this.queue.currentTrack != null, - transitionMode: false, - seek: 0 - } as ResourcePlayOptions, - options - )!; - - if (res && options.queue) { - if (this.queue.hasDebugger) this.queue.debug('Requested option requires to queue the track, adding the given track to queue instead...'); - return this.queue.addTrack(res); - } - - const track = res || this.queue.tracks.dispatch(); - if (!track) { - const error = Exceptions.ERR_NO_RESULT('Play request received but track was not provided'); - this.queue.emit(GuildQueueEvent.error, this.queue, error); - return; - } - - if (this.queue.hasDebugger) this.queue.debug('Requested option requires to play the track, initializing...'); - - try { - if (this.queue.hasDebugger) this.queue.debug(`Initiating stream extraction process...`); - const src = track.raw?.source || track.source; - const qt: SearchQueryType = track.queryType || (src === 'spotify' ? 'spotifySong' : src === 'apple_music' ? 'appleMusicSong' : src); - if (this.queue.hasDebugger) this.queue.debug(`Executing onBeforeCreateStream hook (QueryType: ${qt})...`); - - const streamSrc = { - error: null as Error | null, - stream: null as ExtractorStreamable | null - }; - - await this.queue.onBeforeCreateStream?.(track, qt || 'arbitrary', this.queue).then( - (s) => { - if (s) { - streamSrc.stream = s; - } - }, - (e: Error) => (streamSrc.error = e) - ); - - // throw if 'onBeforeCreateStream' panics - if (!streamSrc.stream && streamSrc.error) return this.#throw(track, streamSrc.error); - - // default behavior when 'onBeforeCreateStream' did not panic - if (!streamSrc.stream) { - if (this.queue.hasDebugger) this.queue.debug('Failed to get stream from onBeforeCreateStream!'); - await this.#createGenericStream(track).then( - (r) => { - if (r?.result) { - streamSrc.stream = r.result; - return; - } - - if (r?.error) { - streamSrc.error = r.error; - return; - } - - streamSrc.stream = streamSrc.error = null; - }, - (e: Error) => (streamSrc.error = e) - ); - } - - if (!streamSrc.stream) return this.#throw(track, streamSrc.error); - - if (typeof options.seek === 'number' && options.seek >= 0) { - this.#progress = options.seek; - } else { - this.#progress = 0; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const cookies = track.raw?.source === 'youtube' ? (this.queue.player.options.ytdlOptions?.requestOptions)?.headers?.cookie : undefined; - - const trackStreamConfig: StreamConfig = { - dispatcherConfig: { - disableBiquad: this.queue.options.disableBiquad, - disableEqualizer: this.queue.options.disableEqualizer, - disableVolume: this.queue.options.disableVolume, - disableFilters: this.queue.options.disableFilterer, - disableResampler: this.queue.options.disableResampler, - sampleRate: typeof this.queue.options.resampler === 'number' && this.queue.options.resampler > 0 ? this.queue.options.resampler : undefined, - biquadFilter: this.queue.filters._lastFiltersCache.biquad || undefined, - eq: this.queue.filters._lastFiltersCache.equalizer, - defaultFilters: this.queue.filters._lastFiltersCache.filters, - volume: this.queue.filters._lastFiltersCache.volume, - data: track, - type: StreamType.Raw, - skipFFmpeg: this.queue.player.options.skipFFmpeg - }, - playerConfig: options - }; - - let resolver: () => void = Util.noop; - const donePromise = new Promise((resolve) => (resolver = resolve)); - - const success = this.queue.emit(GuildQueueEvent.willPlayTrack, this.queue, track, trackStreamConfig, resolver!); - - // prevent dangling promise - if (!success) resolver(); - - if (this.queue.hasDebugger) this.queue.debug('Waiting for willPlayTrack event to resolve...'); - - await donePromise; - - // prettier-ignore - const daspDisabled = [ - trackStreamConfig.dispatcherConfig.disableBiquad, - trackStreamConfig.dispatcherConfig.disableEqualizer, - trackStreamConfig.dispatcherConfig.disableFilters, - trackStreamConfig.dispatcherConfig.disableResampler, - trackStreamConfig.dispatcherConfig.disableVolume - ].every((e) => !!e === true); - - const needsFilters = !!trackStreamConfig.playerConfig.seek || !!this.queue.filters.ffmpeg.args.length; - const shouldSkipFFmpeg = !!trackStreamConfig.dispatcherConfig.skipFFmpeg && !needsFilters; - - let finalStream: Readable; - - const demuxable = (fmt: string) => [StreamType.Opus, StreamType.WebmOpus, StreamType.OggOpus].includes(fmt as StreamType); - - // skip ffmpeg when possible - if (shouldSkipFFmpeg && !(streamSrc.stream instanceof Readable) && typeof streamSrc.stream !== 'string' && demuxable(streamSrc.stream.$fmt)) { - const { $fmt, stream } = streamSrc.stream; - const shouldPCM = !daspDisabled; - - if (this.queue.hasDebugger) this.queue.debug(`skipFFmpeg is set to true and stream is demuxable, creating stream with type ${shouldPCM ? 'pcm' : 'opus'}`); - - // prettier-ignore - const opusStream = $fmt === StreamType.Opus ? - stream : - $fmt === StreamType.OggOpus ? - stream.pipe(new prism.opus.OggDemuxer()) : - stream.pipe(new prism.opus.WebmDemuxer()); - - if (shouldPCM) { - // if we have any filters enabled, we need to decode the opus stream to pcm - finalStream = opusStream.pipe( - new OpusDecoder({ - channels: 2, - frameSize: 960, - rate: 48000 - }) - ); - trackStreamConfig.dispatcherConfig.type = StreamType.Raw; - } else { - finalStream = opusStream; - trackStreamConfig.dispatcherConfig.type = StreamType.Opus; - } - } else { - // const opus = daspDisabled && this.#hasFFmpegOptimization; - // if (opus && this.queue.hasDebugger) this.queue.debug('Disabling PCM output since all filters are disabled and opus encoding is supported...'); - - finalStream = this.#createFFmpegStream( - streamSrc.stream instanceof Readable || typeof streamSrc.stream === 'string' ? streamSrc.stream : streamSrc.stream.stream, - track, - options.seek ?? 0, - cookies - // opus - ); - trackStreamConfig.dispatcherConfig.type = StreamType.Raw; - // FIXME: OggOpus results in static noise - // trackStreamConfig.dispatcherConfig.type = opus ? StreamType.OggOpus : StreamType.Raw; - } - - if (options.transitionMode) { - if (this.queue.hasDebugger) this.queue.debug(`Transition mode detected, player will wait for buffering timeout to expire (Timeout: ${this.queue.options.bufferingTimeout}ms)`); - await waitFor(this.queue.options.bufferingTimeout); - if (this.queue.hasDebugger) this.queue.debug('Buffering timeout has expired!'); - } - - if (this.queue.hasDebugger) this.queue.debug(`Preparing final stream config: ${JSON.stringify(trackStreamConfig, null, 2)}`); - - const dispatcher = this.queue.dispatcher; - - if (!dispatcher) { - if (this.queue.hasDebugger) { - this.queue.debug('Dispatcher is not available, this is most likely due to the queue being deleted in the middle of operation. Cancelling the stream...'); - } - - finalStream.destroy(); - } else { - const resource = await dispatcher.createStream(finalStream, trackStreamConfig.dispatcherConfig); - - this.queue.setTransitioning(!!options.transitionMode); - - await this.#performPlay(resource); - } - } catch (e) { - if (this.queue.hasDebugger) this.queue.debug(`Failed to initialize audio player: ${e}`); - throw e; - } - } - - #throw(track: Track, error?: Error | null) { - // prettier-ignore - const streamDefinitelyFailedMyDearT_TPleaseTrustMeItsNotMyFault = ( - Exceptions.ERR_NO_RESULT(`Could not extract stream for this track${error ? `\n\n${error.stack || error}` : ''}`) - ); - - this.queue.emit(GuildQueueEvent.playerSkip, this.queue, track, TrackSkipReason.NoStream, streamDefinitelyFailedMyDearT_TPleaseTrustMeItsNotMyFault.message); - this.queue.emit(GuildQueueEvent.playerError, this.queue, streamDefinitelyFailedMyDearT_TPleaseTrustMeItsNotMyFault, track); - const nextTrack = this.queue.tracks.dispatch(); - if (nextTrack) this.play(nextTrack, { queue: false }); - return; - } - - async #performPlay(resource: AudioResource) { - if (!this.queue.dispatcher) { - if (this.queue.hasDebugger) { - this.queue.debug('Dispatcher is not available, this is most likely due to the queue being deleted in the middle of operation. Cancelling the stream...'); - } - } else { - if (this.queue.hasDebugger) this.queue.debug('Initializing audio player...'); - await this.queue.dispatcher.playStream(resource); - if (this.queue.hasDebugger) this.queue.debug('Dispatching audio...'); - } - } - - async #createGenericStream(track: Track) { - if (this.queue.hasDebugger) this.queue.debug(`Attempting to extract stream for Track { title: ${track.title}, url: ${track.url} } using registered extractors`); - const streamInfo = await this.queue.player.extractors.run(async (extractor) => { - if (this.queue.player.options.blockStreamFrom?.some((ext) => ext === extractor.identifier)) return false; - const canStream = await extractor.validate(track.url, track.queryType || QueryResolver.resolve(track.url).type); - if (!canStream) return false; - return await extractor.stream(track); - }, false); - if (!streamInfo || !streamInfo.result) { - if (this.queue.hasDebugger) this.queue.debug(`Failed to extract stream for Track { title: ${track.title}, url: ${track.url} } using registered extractors`); - return streamInfo || null; - } - - if (this.queue.hasDebugger) - this.queue.debug(`Stream extraction was successful for Track { title: ${track.title}, url: ${track.url} } (Extractor: ${streamInfo.extractor?.identifier || 'N/A'})`); - - return streamInfo; - } - - #createFFmpegStream(stream: Readable | string, track: Track, seek = 0, cookies?: string, opus?: boolean) { - const ffmpegStream = this.queue.filters.ffmpeg - .createStream(stream, { - encoderArgs: this.queue.filters.ffmpeg.args, - seek: seek / 1000, - fmt: opus ? 'opus' : 's16le', - cookies, - useLegacyFFmpeg: !!this.queue.player.options.useLegacyFFmpeg - }) - .on('error', (err) => { - const m = `${err}`.toLowerCase(); - - if (this.queue.hasDebugger) this.queue.debug(`Stream closed due to an error from FFmpeg stream: ${err.stack || err.message || err}`); - - if (m.includes('premature close') || m.includes('epipe')) return; - - this.queue.emit(GuildQueueEvent.playerError, this.queue, err, track); - }); - - return ffmpegStream; - } -} diff --git a/packages/discord-player/src/queue/GuildQueueStatistics.ts b/packages/discord-player/src/queue/GuildQueueStatistics.ts deleted file mode 100644 index 3a4b37b5f3..0000000000 --- a/packages/discord-player/src/queue/GuildQueueStatistics.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { GuildQueue } from './GuildQueue'; - -export interface GuildQueueStatisticsMetadata { - latency: { - eventLoop: number; - voiceConnection: number; - }; - status: { - buffering: boolean; - playing: boolean; - paused: boolean; - idle: boolean; - }; - tracksCount: number; - historySize: number; - extractors: number; - listeners: number; - memoryUsage: NodeJS.MemoryUsage; - versions: { - node: string; - player: string; - }; -} - -export class GuildQueueStatistics { - public constructor(public queue: GuildQueue) {} - - /** - * Generate statistics of this queue - */ - public generate() { - return { - latency: { - eventLoop: this.queue.player.eventLoopLag, - voiceConnection: this.queue.ping - }, - status: { - buffering: this.queue.node.isBuffering(), - playing: this.queue.node.isPlaying(), - paused: this.queue.node.isPaused(), - idle: this.queue.node.isIdle() - }, - tracksCount: this.queue.tracks.size, - historySize: this.queue.history.tracks.size, - extractors: this.queue.player.extractors.size, - listeners: this.queue.guild.members.me?.voice.channel?.members.filter((m) => !m.user.bot).size || 0, - memoryUsage: process.memoryUsage(), - versions: { - node: process.version, - player: '[VI]{{inject}}[/VI]' - } - } as GuildQueueStatisticsMetadata; - } -} diff --git a/packages/discord-player/src/queue/SyncedLyricsProvider.ts b/packages/discord-player/src/queue/SyncedLyricsProvider.ts deleted file mode 100644 index 0214bd3caf..0000000000 --- a/packages/discord-player/src/queue/SyncedLyricsProvider.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Exceptions } from '../errors'; -import { LrcGetResult, LrcSearchResult } from '../lrclib/LrcLib'; -import type { GuildQueue } from './GuildQueue'; - -export type LyricsData = Map; -export type Unsubscribe = () => void; -export type LyricsCallback = (lyrics: string, timestamp: number) => unknown; -export type LyricsAt = { timestamp: number; line: string }; - -const timestampPattern = /\[(\d{2}):(\d{2})\.(\d{2})\]/; - -export class SyncedLyricsProvider { - #loop: NodeJS.Timer | null = null; - #callback: LyricsCallback | null = null; - #onUnsubscribe: Unsubscribe | null = null; - - public interval = 100; - public readonly lyrics: LyricsData = new Map(); - - public constructor(public readonly queue: GuildQueue, public readonly raw?: LrcGetResult | LrcSearchResult) { - if (raw?.syncedLyrics) this.load(raw?.syncedLyrics); - } - - public isSubscribed() { - return this.#callback !== null; - } - - public load(lyrics: string) { - if (!lyrics) throw Exceptions.ERR_NOT_EXISTING('syncedLyrics'); - - this.lyrics.clear(); - this.unsubscribe(); - - const lines = lyrics.split('\n'); - - for (const line of lines) { - const match = line.match(timestampPattern); - - if (match) { - const [, minutes, seconds, milliseconds] = match; - const timestamp = parseInt(minutes) * 60 * 1000 + parseInt(seconds) * 1000 + parseInt(milliseconds); - - this.lyrics.set(timestamp, line.replace(timestampPattern, '').trim()); - } - } - } - - /** - * Returns the lyrics at a specific time or at the closest time (±2 seconds) - * @param time The time in milliseconds - */ - public at(time: number): LyricsAt | null { - const lowestTime = this.lyrics.keys().next().value; - if (lowestTime == null || time < lowestTime) return null; - if (this.lyrics.has(time)) return { line: this.lyrics.get(time) as string, timestamp: time }; - - const keys = Array.from(this.lyrics.keys()); - - const closest = keys.reduce((a, b) => (Math.abs(b - time) < Math.abs(a - time) ? b : a)); - - if (closest > time) return null; - - if (Math.abs(closest - time) > 2000) return null; - - const line = this.lyrics.get(closest); - - if (!line) return null; - - return { timestamp: closest, line }; - } - - /** - * Callback for the lyrics change. - * @param callback The callback function - */ - public onChange(callback: LyricsCallback) { - this.#callback = callback; - } - - /** - * Callback to detect when the provider is unsubscribed. - * @param callback The callback function - */ - public onUnsubscribe(callback: Unsubscribe) { - this.#onUnsubscribe = callback; - } - - /** - * Unsubscribes from the queue. - */ - public unsubscribe() { - if (this.#loop) clearInterval(this.#loop); - if (this.#onUnsubscribe) this.#onUnsubscribe(); - - this.#callback = null; - this.#onUnsubscribe = null; - this.#loop = null; - } - - /** - * Subscribes to the queue to monitor the current time. - * @returns The unsubscribe function - */ - public subscribe(): Unsubscribe { - if (this.#loop) return () => this.unsubscribe(); - - this.#createLoop(); - - return () => this.unsubscribe(); - } - - /** - * Pauses the lyrics provider. - */ - public pause() { - const hasLoop = this.#loop !== null; - - if (hasLoop) { - clearInterval(this.#loop as NodeJS.Timer); - this.#loop = null; - } - - return hasLoop; - } - - /** - * Resumes the lyrics provider. - */ - public resume() { - const hasLoop = this.#loop !== null; - - if (!hasLoop) this.#createLoop(); - - return !hasLoop; - } - - #createLoop() { - if (!this.#callback) return; - if (this.#loop) clearInterval(this.#loop); - - let lastValue: LyricsAt | null = null; - - this.#loop = setInterval(() => { - if (this.queue.deleted) return this.unsubscribe(); - - if (!this.#callback || !this.queue.isPlaying()) return; - - const time = this.queue.node.getTimestamp(); - if (!time) return; - - const lyrics = this.at(time.current.value); - - if (!lyrics) return; - - if (lastValue !== null && lyrics.line === lastValue.line && lyrics.timestamp === lastValue.timestamp) return; - - lastValue = lyrics; - - this.#callback(lyrics.line, lyrics.timestamp); - }, this.interval).unref(); - } -} diff --git a/packages/discord-player/src/queue/VoiceReceiverNode.ts b/packages/discord-player/src/queue/VoiceReceiverNode.ts deleted file mode 100644 index cf79d847a9..0000000000 --- a/packages/discord-player/src/queue/VoiceReceiverNode.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { UserResolvable } from 'discord.js'; -import { PassThrough, type Readable } from 'stream'; -import { EndBehaviorType } from 'discord-voip'; -import * as prism from 'prism-media'; -import { StreamDispatcher } from '../VoiceInterface/StreamDispatcher'; -import { Track } from '../fabric/Track'; -import { RawTrackData } from '../types/types'; -import { Exceptions } from '../errors'; - -export interface VoiceReceiverOptions { - mode?: 'opus' | 'pcm'; - end?: EndBehaviorType; - silenceDuration?: number; - crc?: boolean; -} - -export type RawTrackInit = Partial>; - -export class VoiceReceiverNode { - public constructor(public dispatcher: StreamDispatcher) {} - - public createRawTrack(stream: Readable, data: RawTrackInit = {}) { - data.title ??= `Recording ${Date.now()}`; - - return new Track(this.dispatcher.queue.player, { - author: 'Discord', - description: data.title, - title: data.title, - duration: data.duration || '0:00', - views: 0, - requestedBy: data.requestedBy, - thumbnail: data.thumbnail || 'https://cdn.discordapp.com/embed/avatars/0.png', - url: data.url || 'https://discord.com', - source: 'arbitrary', - raw: { - engine: stream, - source: 'arbitrary' - } - }); - } - - /** - * Merge multiple streams together - * @param streams The array of streams to merge - */ - public mergeRecordings(streams: Readable[]) { - // TODO - void streams; - throw Exceptions.ERR_NOT_IMPLEMENTED(`${this.constructor.name}.mergeRecordings()`); - } - - /** - * Record a user in voice channel - * @param user The user to record - * @param options Recording options - */ - public recordUser( - user: UserResolvable, - options: VoiceReceiverOptions = { - end: EndBehaviorType.AfterSilence, - mode: 'pcm', - silenceDuration: 1000 - } - ) { - const _user = this.dispatcher.queue.player.client.users.resolveId(user); - - const passThrough = new PassThrough(); - const receiver = this.dispatcher.voiceConnection.receiver; - - if (!receiver) throw Exceptions.ERR_NO_RECEIVER(); - - receiver.speaking.on('start', (userId) => { - if (userId === _user) { - const receiveStream = receiver.subscribe(_user, { - end: { - behavior: options.end || EndBehaviorType.AfterSilence, - duration: options.silenceDuration ?? 1000 - } - }); - - setImmediate(async () => { - if (options.mode === 'pcm') { - const pcm = receiveStream.pipe( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - new (prism.opus || (prism).default.opus).Decoder({ - channels: 2, - frameSize: 960, - rate: 48000 - }) - ); - return pcm.pipe(passThrough); - } else { - return receiveStream.pipe(passThrough); - } - }).unref(); - } - }); - - return passThrough as Readable; - } -} diff --git a/packages/discord-player/src/queue/index.ts b/packages/discord-player/src/queue/index.ts deleted file mode 100644 index 1dee13e03c..0000000000 --- a/packages/discord-player/src/queue/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './GuildNodeManager'; -export * from './GuildQueue'; -export * from './GuildQueueAudioFilters'; -export * from './GuildQueueHistory'; -export * from './GuildQueuePlayerNode'; -export * from './VoiceReceiverNode'; -export * from './GuildQueueStatistics'; diff --git a/packages/discord-player/src/types/types.ts b/packages/discord-player/src/types/types.ts deleted file mode 100644 index f66e5df006..0000000000 --- a/packages/discord-player/src/types/types.ts +++ /dev/null @@ -1,534 +0,0 @@ -import type { User, UserResolvable, VoiceState } from 'discord.js'; -import type { GuildQueue } from '../queue'; -import type { Track } from '../fabric/Track'; -import type { Playlist } from '../fabric/Playlist'; -import type { downloadOptions } from 'ytdl-core'; -import type { QueryCacheProvider } from '../utils/QueryCache'; -import type { IPRotationConfig } from '../utils/IPRotator'; - -// @ts-ignore -import type { BridgeProvider } from '@discord-player/extractor'; - -export type FiltersName = keyof QueueFilters; - -export interface PlayerSearchResult { - playlist: Playlist | null; - tracks: Track[]; -} - -/** - * Represents FFmpeg filters - */ -export interface QueueFilters { - bassboost_low?: boolean; - bassboost?: boolean; - bassboost_high?: boolean; - '8D'?: boolean; - vaporwave?: boolean; - nightcore?: boolean; - phaser?: boolean; - tremolo?: boolean; - vibrato?: boolean; - reverse?: boolean; - treble?: boolean; - normalizer?: boolean; - normalizer2?: boolean; - surrounding?: boolean; - pulsator?: boolean; - subboost?: boolean; - karaoke?: boolean; - flanger?: boolean; - gate?: boolean; - haas?: boolean; - mcompand?: boolean; - mono?: boolean; - mstlr?: boolean; - mstrr?: boolean; - compressor?: boolean; - expander?: boolean; - softlimiter?: boolean; - chorus?: boolean; - chorus2d?: boolean; - chorus3d?: boolean; - fadein?: boolean; - dim?: boolean; - earrape?: boolean; - lofi?: boolean; - silenceremove?: boolean; -} - -/** - * The track source: - * - soundcloud - * - youtube - * - spotify - * - apple_music - * - arbitrary - */ -export type TrackSource = 'soundcloud' | 'youtube' | 'spotify' | 'apple_music' | 'arbitrary'; - -export interface RawTrackData { - /** - * The title - */ - title: string; - /** - * The description - */ - description: string; - /** - * The author - */ - author: string; - /** - * The url - */ - url: string; - /** - * The thumbnail - */ - thumbnail: string; - /** - * The duration - */ - duration: string; - /** - * The duration in ms - */ - views: number; - /** - * The user who requested this track - */ - requestedBy?: User | null; - /** - * The playlist - */ - playlist?: Playlist; - /** - * The source - */ - source?: TrackSource; - /** - * The engine - */ - engine?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - /** - * If this track is live - */ - live?: boolean; - /** - * The raw data - */ - raw?: any; // eslint-disable-line @typescript-eslint/no-explicit-any - /** - * The query type - */ - queryType?: SearchQueryType; - /** - * The seralised title - */ - cleanTitle?: string; -} - -export interface TimeData { - /** - * Time in days - */ - days: number; - /** - * Time in hours - */ - hours: number; - /** - * Time in minutes - */ - minutes: number; - /** - * Time in seconds - */ - seconds: number; -} - -export interface PlayerProgressbarOptions { - /** - * If it should render time codes - */ - timecodes?: boolean; - /** - * If it should create progress bar for the whole queue - */ - length?: number; - /** - * The bar length - */ - leftChar?: string; - /** - * The elapsed time track - */ - rightChar?: string; - /** - * The remaining time track - */ - separator?: string; - /** - * The separation between timestamp and line - */ - indicator?: string; - /** - * The indicator - */ - queue?: boolean; -} - -/** - * The search query type - * This can be one of: - * - AUTO - * - YOUTUBE - * - YOUTUBE_PLAYLIST - * - SOUNDCLOUD_TRACK - * - SOUNDCLOUD_PLAYLIST - * - SOUNDCLOUD - * - SPOTIFY_SONG - * - SPOTIFY_ALBUM - * - SPOTIFY_PLAYLIST - * - SPOTIFY_SEARCH - * - FACEBOOK - * - VIMEO - * - ARBITRARY - * - REVERBNATION - * - YOUTUBE_SEARCH - * - YOUTUBE_VIDEO - * - SOUNDCLOUD_SEARCH - * - APPLE_MUSIC_SONG - * - APPLE_MUSIC_ALBUM - * - APPLE_MUSIC_PLAYLIST - * - APPLE_MUSIC_SEARCH - * - FILE - * - AUTO_SEARCH - * @typedef {string} QueryType - */ -export const QueryType = { - AUTO: 'auto', - YOUTUBE: 'youtube', - YOUTUBE_PLAYLIST: 'youtubePlaylist', - SOUNDCLOUD_TRACK: 'soundcloudTrack', - SOUNDCLOUD_PLAYLIST: 'soundcloudPlaylist', - SOUNDCLOUD: 'soundcloud', - SPOTIFY_SONG: 'spotifySong', - SPOTIFY_ALBUM: 'spotifyAlbum', - SPOTIFY_PLAYLIST: 'spotifyPlaylist', - SPOTIFY_SEARCH: 'spotifySearch', - FACEBOOK: 'facebook', - VIMEO: 'vimeo', - ARBITRARY: 'arbitrary', - REVERBNATION: 'reverbnation', - YOUTUBE_SEARCH: 'youtubeSearch', - YOUTUBE_VIDEO: 'youtubeVideo', - SOUNDCLOUD_SEARCH: 'soundcloudSearch', - APPLE_MUSIC_SONG: 'appleMusicSong', - APPLE_MUSIC_ALBUM: 'appleMusicAlbum', - APPLE_MUSIC_PLAYLIST: 'appleMusicPlaylist', - APPLE_MUSIC_SEARCH: 'appleMusicSearch', - FILE: 'file', - AUTO_SEARCH: 'autoSearch' -} as const; - -export type SearchQueryType = keyof typeof QueryType | (typeof QueryType)[keyof typeof QueryType]; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -export interface PlayerEvents { - debug: (message: string) => any; - error: (error: Error) => any; - voiceStateUpdate: (queue: GuildQueue, oldState: VoiceState, newState: VoiceState) => any; -} - -export const PlayerEvent = { - debug: 'debug', - Debug: 'debug', - error: 'error', - Error: 'error', - voiceStateUpdate: 'voiceStateUpdate', - VoiceStateUpdate: 'voiceStateUpdate' -} as const; -export type PlayerEvent = (typeof PlayerEvent)[keyof typeof PlayerEvent]; - -/* eslint-enable @typescript-eslint/no-explicit-any */ - -export interface PlayOptions { - /** - * If this play was triggered for filters update - */ - filtersUpdate?: boolean; - /** - * FFmpeg args passed to encoder - */ - encoderArgs?: string[]; - /** - * Time to seek to before playing - */ - seek?: number; - /** - * If it should start playing the provided track immediately - */ - immediate?: boolean; -} - -export type QueryExtractorSearch = `ext:${string}`; - -export interface SearchOptions { - /** - * The user who requested this search - */ - requestedBy?: UserResolvable; - /** - * The query search engine, can be extractor name to target specific one (custom) - */ - searchEngine?: SearchQueryType | QueryExtractorSearch; - /** - * List of the extractors to block - */ - blockExtractors?: string[]; - /** - * If it should ignore query cache lookup - */ - ignoreCache?: boolean; - /** - * Fallback search engine to use - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - requestOptions?: any; - /** - * Fallback search engine to use - */ - fallbackSearchEngine?: (typeof QueryType)[keyof typeof QueryType]; -} - -/** - * The queue repeat mode. This can be one of: - * - OFF - * - TRACK - * - QUEUE - * - AUTOPLAY - */ -export enum QueueRepeatMode { - /** - * Disable repeat mode. - */ - OFF = 0, - /** - * Repeat the current track. - */ - TRACK = 1, - /** - * Repeat the entire queue. - */ - QUEUE = 2, - /** - * When last track ends, play similar tracks in the future if queue is empty. - */ - AUTOPLAY = 3 -} - -export interface PlaylistInitData { - /** - * The tracks of this playlist - */ - tracks: Track[]; - /** - * The playlist title - */ - title: string; - /** - * The description - */ - description: string; - /** - * The thumbnail - */ - thumbnail: string; - /** - * The playlist type: `album` | `playlist` - */ - type: 'album' | 'playlist'; - /** - * The playlist source - */ - source: TrackSource; - /** - * The playlist author - */ - author: { - /** - * The author name - */ - name: string; - /** - * The author url - */ - url: string; - }; - /** - * The playlist id - */ - id: string; - /** - * The playlist url - */ - url: string; - /** - * The raw playlist data - */ - rawPlaylist?: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -export interface TrackJSON { - /** - * The track id - */ - id: string; - /** - * The track title - */ - title: string; - /** - * The track description - */ - description: string; - /** - * The track author - */ - author: string; - /** - * The track url - */ - url: string; - /** - * The track thumbnail - */ - thumbnail: string; - /** - * The track duration - */ - duration: string; - /** - * The track duration in ms - */ - durationMS: number; - /** - * The track views - */ - views: number; - /** - * The user id who requested this track - */ - requestedBy: string; - /** - * The playlist info (if any) - */ - playlist?: PlaylistJSON; -} - -export interface PlaylistJSON { - /** - * The playlist id - */ - id: string; - /** - * The playlist url - */ - url: string; - /** - * The playlist title - */ - title: string; - /** - * The playlist description - */ - description: string; - /** - * The thumbnail - */ - thumbnail: string; - /** - * The playlist type: `album` | `playlist` - */ - type: 'album' | 'playlist'; - /** - * The track source - */ - source: TrackSource; - /** - * The playlist author - */ - author: { - /** - * The author name - */ - name: string; - /** - * The author url - */ - url: string; - }; - /** - * The tracks data (if any) - */ - tracks: TrackJSON[]; -} - -export interface PlayerInitOptions { - /** - * The options passed to `ytdl-core`. - */ - ytdlOptions?: downloadOptions; - /** - * The voice connection timeout - */ - connectionTimeout?: number; - /** - * Time in ms to re-monitor event loop lag - */ - lagMonitor?: number; - /** - * Prevent voice state handler from being overridden - */ - lockVoiceStateHandler?: boolean; - /** - * List of extractors to disable querying metadata from - */ - blockExtractors?: string[]; - /** - * List of extractors to disable streaming from - */ - blockStreamFrom?: string[]; - /** - * Query cache provider - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - queryCache?: QueryCacheProvider | null; - /** - * Ignore player instance - */ - ignoreInstance?: boolean; - /** - * Use legacy version of ffmpeg - */ - useLegacyFFmpeg?: boolean; - /** - * Set bridge provider - */ - bridgeProvider?: BridgeProvider; - /** - * IP rotator config - */ - ipconfig?: IPRotationConfig; - /** - * Skip ffmpeg process when possible - */ - skipFFmpeg?: boolean; - /** - * The probe timeout in milliseconds. Defaults to 5000. - */ - probeTimeout?: number; -} diff --git a/packages/discord-player/src/utils/AsyncQueue.ts b/packages/discord-player/src/utils/AsyncQueue.ts deleted file mode 100644 index eab1e5f52c..0000000000 --- a/packages/discord-player/src/utils/AsyncQueue.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { SnowflakeUtil } from 'discord.js'; - -export interface AsyncQueueAcquisitionOptions { - /** - * AbortSignal to cancel this entry - */ - signal?: AbortSignal; -} - -export type AsyncQueueExceptionHandler = (exception: Error) => void; - -export class AsyncQueue { - /** - * The queued entries - */ - public entries: Array = []; - - public exceptionHandler?: AsyncQueueExceptionHandler; - - /** - * Clear entries queue - * @param consume Whether or not to consume all entries before clearing - */ - public clear(consume = false) { - if (consume) { - this.entries.forEach((entry) => entry.consume()); - } - - this.entries = []; - } - - /** - * The total number of entries in this queue. Returns `0` if no entries are available. - */ - public get size() { - return this.entries.length; - } - - /** - * Acquire an entry. - * - * @example // lock the queue - * const entry = asyncQueue.acquire(); - * // wait until previous task is completed - * await entry.getTask(); - * // do something expensive - * await performSomethingExpensive(); - * // make sure to release the lock once done - * asyncQueue.release(); - * - */ - public acquire(options?: AsyncQueueAcquisitionOptions) { - const entry = new AsyncQueueEntry(this, options); - - if (this.exceptionHandler) entry.getTask().catch(this.exceptionHandler); - - if (this.entries.length === 0) { - this.entries.push(entry); - entry.consume(); - return entry; - } - - this.entries.push(entry); - return entry; - } - - /** - * Release the current acquisition and move to next entry. - */ - public release(): void { - if (!this.entries.length) return; - - this.entries.shift(); - this.entries[0]?.consume(); - } - - /** - * Cancel all entries - */ - public cancelAll() { - this.entries.forEach((entry) => entry.cancel()); - } - - /** - * Remove the given entry from the queue - * @param entry The entry to remove - */ - public removeEntry(entry: AsyncQueueEntry) { - const entryIdx = this.entries.indexOf(entry); - - if (entryIdx !== -1) { - this.entries.splice(entryIdx, 1); - return true; - } - - return false; - } -} - -export class AsyncQueueEntry { - public readonly id = SnowflakeUtil.generate().toString(); - private readonly promise: Promise; - public signal: AbortSignal | null = null; - public onAbort: (() => void) | null = null; - private resolve!: () => void; - private reject!: (err: Error) => void; - - public constructor(public queue: AsyncQueue, public options?: AsyncQueueAcquisitionOptions) { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - - if (this.options?.signal) { - this.setAbortSignal(this.options.signal); - } - } - - public setAbortSignal(signal: AbortSignal) { - if (signal.aborted) return; - this.signal = signal; - this.onAbort = () => { - this.queue.removeEntry(this); - this.cancel(); - }; - - this.signal.addEventListener('abort', this.onAbort); - } - - public consume() { - this.cleanup(); - this.resolve(); - } - - public release() { - this.consume(); - this.queue.release(); - } - - public cancel() { - this.cleanup(); - this.reject(new Error('Cancelled')); - } - - public cleanup() { - if (this.onAbort) this.signal?.removeEventListener('abort', this.onAbort); - this.signal = null; - this.onAbort = null; - } - - public getTask() { - return this.promise; - } -} diff --git a/packages/discord-player/src/utils/AudioFilters.ts b/packages/discord-player/src/utils/AudioFilters.ts deleted file mode 100644 index 489c127a58..0000000000 --- a/packages/discord-player/src/utils/AudioFilters.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { FiltersName } from '../types/types'; - -const bass = (g: number) => `bass=g=${g}:f=110:w=0.3`; - -export class AudioFilters { - public constructor() { - return AudioFilters; - } - - public static filters: Record = { - bassboost_low: bass(15), - bassboost: bass(20), - bassboost_high: bass(30), - '8D': 'apulsator=hz=0.09', - vaporwave: 'aresample=48000,asetrate=48000*0.8', - nightcore: 'aresample=48000,asetrate=48000*1.25', - lofi: 'aresample=48000,asetrate=48000*0.9,extrastereo=m=2.5:c=disabled', - phaser: 'aphaser=in_gain=0.4', - tremolo: 'tremolo', - vibrato: 'vibrato=f=6.5', - reverse: 'areverse', - treble: 'treble=g=5', - normalizer2: 'dynaudnorm=g=101', - normalizer: 'acompressor', - surrounding: 'surround', - pulsator: 'apulsator=hz=1', - subboost: 'asubboost', - karaoke: 'stereotools=mlev=0.03', - flanger: 'flanger', - gate: 'agate', - haas: 'haas', - mcompand: 'mcompand', - mono: 'pan=mono|c0=.5*c0+.5*c1', - mstlr: 'stereotools=mode=ms>lr', - mstrr: 'stereotools=mode=ms>rr', - compressor: 'compand=points=-80/-105|-62/-80|-15.4/-15.4|0/-12|20/-7.6', - expander: 'compand=attacks=0:points=-80/-169|-54/-80|-49.5/-64.6|-41.1/-41.1|-25.8/-15|-10.8/-4.5|0/0|20/8.3', - softlimiter: 'compand=attacks=0:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8', - chorus: 'chorus=0.7:0.9:55:0.4:0.25:2', - chorus2d: 'chorus=0.6:0.9:50|60:0.4|0.32:0.25|0.4:2|1.3', - chorus3d: 'chorus=0.5:0.9:50|60|40:0.4|0.32|0.3:0.25|0.4|0.3:2|2.3|1.3', - fadein: 'afade=t=in:ss=0:d=10', - dim: `afftfilt="'real=re * (1-clip((b/nb)*b,0,1))':imag='im * (1-clip((b/nb)*b,0,1))'"`, - earrape: 'channelsplit,sidechaingate=level_in=64', - silenceremove: 'silenceremove=1:0:-50dB' - }; - - public static get(name: K) { - return this.filters[name] ?? name; - } - - public static has(name: K) { - return name in this.filters; - } - - public static *[Symbol.iterator](): IterableIterator<{ name: FiltersName; value: string }> { - for (const [k, v] of Object.entries(this.filters)) { - yield { name: k as FiltersName, value: v as string }; - } - } - - public static get names() { - return Object.keys(this.filters) as FiltersName[]; - } - - // @ts-ignore - public static get length() { - return this.names.length; - } - - public static toString() { - return this.names.map((m) => (this as any)[m]).join(','); // eslint-disable-line @typescript-eslint/no-explicit-any - } - - /** - * Create ffmpeg args from the specified filters name - * @param filter The filter name - * @returns - */ - public static create(filters?: (K | string)[]) { - if (!filters || !Array.isArray(filters)) return this.toString(); - return filters - .filter((predicate) => typeof predicate === 'string') - .map((m) => this.get(m as K)) - .join(','); - } - - /** - * Defines audio filter - * @param filterName The name of the filter - * @param value The ffmpeg args - */ - public static define(filterName: string, value: string) { - this.filters[filterName as FiltersName] = value; - } - - /** - * Defines multiple audio filters - * @param filtersArray Array of filters containing the filter name and ffmpeg args - */ - public static defineBulk(filtersArray: { name: string; value: string }[]) { - filtersArray.forEach((arr) => this.define(arr.name, arr.value)); - } -} diff --git a/packages/discord-player/src/utils/FFmpegStream.ts b/packages/discord-player/src/utils/FFmpegStream.ts deleted file mode 100644 index 846c463c40..0000000000 --- a/packages/discord-player/src/utils/FFmpegStream.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { Duplex, Readable } from 'stream'; -import * as prism from 'prism-media'; -import { FFmpeg } from '@discord-player/ffmpeg'; - -export interface FFmpegStreamOptions { - fmt?: string; - encoderArgs?: string[]; - seek?: number; - skip?: boolean; - cookies?: string; - useLegacyFFmpeg?: boolean; -} - -const getFFmpegProvider = (legacy = false) => (legacy ? (prism as typeof prism & { default: typeof prism }).default?.FFmpeg || prism.FFmpeg : FFmpeg); - -const resolveArgs = (config: Record): string[] => { - return Object.entries(config).reduce((acc, [key, value]) => { - if (value == null) return acc; - acc.push(`-${key}`, String(value)); - return acc; - }, [] as string[]); -}; - -export function FFMPEG_ARGS_STRING(stream: string, fmt?: string, cookies?: string) { - const args = resolveArgs({ - reconnect: 1, - reconnect_streamed: 1, - reconnect_delay_max: 5, - i: stream, - analyzeduration: 0, - loglevel: 0, - ar: 48000, - ac: 2, - f: `${typeof fmt === 'string' ? fmt : 's16le'}`, - acodec: fmt === 'opus' ? 'libopus' : null, - cookies: typeof cookies === 'string' ? (!cookies.includes(' ') ? cookies : `"${cookies}"`) : null - }); - - return args; -} - -export function FFMPEG_ARGS_PIPED(fmt?: string) { - const args = resolveArgs({ - analyzeduration: 0, - loglevel: 0, - ar: 48000, - ac: 2, - f: `${typeof fmt === 'string' ? fmt : 's16le'}`, - acodec: fmt === 'opus' ? 'libopus' : null - }); - - return args; -} - -/** - * Creates FFmpeg stream - * @param stream The source stream - * @param options FFmpeg stream options - */ -export function createFFmpegStream(stream: Readable | Duplex | string, options?: FFmpegStreamOptions) { - if (options?.skip && typeof stream !== 'string') return stream; - options ??= {}; - const args = typeof stream === 'string' ? FFMPEG_ARGS_STRING(stream, options.fmt, options.cookies) : FFMPEG_ARGS_PIPED(options.fmt); - - if (!Number.isNaN(options.seek)) args.unshift('-ss', String(options.seek)); - if (Array.isArray(options.encoderArgs)) args.push(...options.encoderArgs); - - const FFMPEG = getFFmpegProvider(!!options.useLegacyFFmpeg); - - const transcoder = new FFMPEG({ shell: false, args }); - - transcoder.on('close', () => transcoder.destroy()); - - if (typeof stream !== 'string') { - stream.on('error', () => transcoder.destroy()); - stream.pipe(transcoder); - } - - return transcoder; -} diff --git a/packages/discord-player/src/utils/IPRotator.ts b/packages/discord-player/src/utils/IPRotator.ts deleted file mode 100644 index 4f1808cf1b..0000000000 --- a/packages/discord-player/src/utils/IPRotator.ts +++ /dev/null @@ -1,133 +0,0 @@ -import ip from 'ip'; - -export class IPBlock { - public usage = 0; - public readonly cidr: string; - public readonly cidrSize: number; - - public constructor(public block: string) { - if (ip.isV4Format(block.split('/')[0]) && !block.includes('/')) { - block += '/32'; - } else if (ip.isV6Format(block.split('/')[0]) && !block.includes('/')) { - block += '/128'; - } - - this.cidr = ip.cidr(this.block); - this.cidrSize = ip.cidrSubnet(this.block).subnetMaskLength; - } - - public consume() { - this.usage++; - } -} - -export interface IPRotationConfig { - /** - * IP blocks to use - */ - blocks: string[]; - /** - * IPs to exclude - */ - exclude?: string[]; - /** - * Max retries to find an IP that is not excluded - */ - maxRetries?: number; -} - -export class IPRotator { - public blocks: IPBlock[] = []; - public failures = new Map(); - public MAX_NEXT_RETRIES = 30; - #retries = 0; - - public constructor(public config: IPRotationConfig) { - config.exclude ??= []; - this.blocks = config.blocks.map((block) => new IPBlock(block)); - this.MAX_NEXT_RETRIES = config.maxRetries ?? 10; - } - - public getIP(): { ip: string; family: 4 | 6 } { - const block = this.blocks.sort((a, b) => a.usage - b.usage)[0]; - if (!block) { - throw new Error('No IP blocks available'); - } - - const random = IPRotator.getRandomIP(block.cidr, block.cidrSize); - - if (this.isFailedOrExcluded(random)) { - if (this.#retries++ > this.MAX_NEXT_RETRIES) { - this.#retries = 0; - throw new Error('Unable to find an IP that is not excluded'); - } - - return this.getIP(); - } - - this.#retries = 0; - block.consume(); - return { ip: random, family: ip.isV4Format(random) ? 4 : 6 }; - } - - public isFailedOrExcluded(ip: string) { - return this.failures.has(ip) || !!this.config.exclude?.includes(ip); - } - - public addFailed(ip: string) { - const lastFailedCount = this.failures.get(ip) ?? 0; - - this.failures.set(ip, lastFailedCount + 1); - } - - public static getRandomIP(address: string, start?: number, end?: number) { - // Author: Jesse Tane - // NPMJS: https://npmjs.org/random-ip - - const bytes = ip.toBuffer(address); - const ipv6 = bytes.length === 16; - const bytesize = 8; - - start = start || 0; - end = typeof end !== 'undefined' ? end : bytes.length * bytesize; - - for (let i = 0; i < bytes.length; i++) { - let bit = i * bytesize; - - if (bit + bytesize < start || bit >= end) { - continue; - } - - let b = bytes[i]; - - for (let n = 0; n < bytesize; n++) { - if (bit >= start && bit < end) { - const bitpos = bytesize - n - 1; - const bitmask = 1 << bitpos; - if (Math.random() < 0.5) { - b |= bitmask; - } else { - b &= ~bitmask; - } - } - bit++; - } - - bytes[i] = b; - } - - const tets = []; - - for (let i = 0; i < bytes.length; i++) { - if (ipv6) { - if (i % 2 === 0) { - tets[i >> 1] = ((bytes[i] << bytesize) | bytes[i + 1]).toString(16); - } - } else { - tets[i] = bytes[i]; - } - } - - return tets.join(ipv6 ? ':' : '.'); - } -} diff --git a/packages/discord-player/src/utils/PlayerEventsEmitter.ts b/packages/discord-player/src/utils/PlayerEventsEmitter.ts deleted file mode 100644 index 6424c096bc..0000000000 --- a/packages/discord-player/src/utils/PlayerEventsEmitter.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { DefaultListener } from '@discord-player/utils'; -import { ListenerSignature } from '@discord-player/utils'; -import { EventEmitter } from '@discord-player/utils'; -import { Util } from './Util'; - -export class PlayerEventsEmitter = DefaultListener> extends EventEmitter { - #hasDebugger = false; - public constructor(public requiredEvents: Array = []) { - super(); - } - - public on(name: K, listener: L[K]) { - if (name === 'debug') { - this.#hasDebugger = true; - } - - return super.on(name, listener); - } - - public once(name: K, listener: L[K]) { - if (name === 'debug') { - this.#hasDebugger = true; - } - - return super.once(name, listener); - } - - public addListener(name: K, listener: L[K]) { - if (name === 'debug') { - this.#hasDebugger = true; - } - - return super.addListener(name, listener); - } - - public off(name: K, listener: L[K]) { - this.#hasDebugger = this.listenerCount('debug' as K) > 0; - - return super.off(name, listener); - } - - public removeListener(name: K, listener: L[K]) { - this.#hasDebugger = this.listenerCount('debug' as K) > 0; - - return super.removeListener(name, listener); - } - - public removeAllListeners(name?: K) { - this.#hasDebugger = this.listenerCount('debug' as K) > 0; - - return super.removeAllListeners(name); - } - - public emit(name: K, ...args: Parameters) { - if (this.requiredEvents.includes(name) && !this.eventNames().includes(name)) { - // eslint-disable-next-line no-console - console.error(...args); - Util.warn( - `No event listener found for event "${String(name)}". Events ${this.requiredEvents.map((m) => `"${String(m)}"`).join(', ')} must have event listeners.`, - 'UnhandledEventsWarning' - ); - return false; - } - - return super.emit(name, ...args); - } - - public get hasDebugger() { - return this.#hasDebugger; - } -} diff --git a/packages/discord-player/src/utils/QueryCache.ts b/packages/discord-player/src/utils/QueryCache.ts deleted file mode 100644 index 84d393a794..0000000000 --- a/packages/discord-player/src/utils/QueryCache.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Player } from '../Player'; -import { SearchResult } from '../fabric/SearchResult'; -import { Track } from '../fabric/Track'; -import { User } from 'discord.js'; -import { SearchQueryType } from '../types/types'; - -export interface QueryCacheOptions { - checkInterval?: number; -} - -// 5h -const DEFAULT_EXPIRY_TIMEOUT = 18_000_000; - -export interface QueryCacheProvider { - getData(): Promise[]>; - addData(data: SearchResult): Promise; - resolve(context: QueryCacheResolverContext): Promise; -} - -export class QueryCache implements QueryCacheProvider { - #defaultCache = new Map>(); - public timer: NodeJS.Timer; - public constructor( - public player: Player, - public options: QueryCacheOptions = { - checkInterval: DEFAULT_EXPIRY_TIMEOUT - } - ) { - this.timer = setInterval(this.cleanup.bind(this), this.checkInterval).unref(); - } - - public get checkInterval() { - return this.options.checkInterval ?? DEFAULT_EXPIRY_TIMEOUT; - } - - public async cleanup() { - for (const [id, value] of this.#defaultCache) { - if (value.hasExpired()) { - this.#defaultCache.delete(id); - } - } - } - - public async clear() { - this.#defaultCache.clear(); - } - - public async getData() { - return [...this.#defaultCache.values()]; - } - - public async addData(data: SearchResult) { - data.tracks.forEach((d) => { - if (this.#defaultCache.has(d.url)) return; - this.#defaultCache.set(d.url, new DiscordPlayerQueryResultCache(d)); - }); - } - - public async resolve(context: QueryCacheResolverContext) { - const result = this.#defaultCache.get(context.query); - if (!result) - return new SearchResult(this.player, { - query: context.query, - requestedBy: context.requestedBy, - queryType: context.queryType - }); - - return new SearchResult(this.player, { - query: context.query, - tracks: [result.data], - playlist: null, - queryType: context.queryType, - requestedBy: context.requestedBy - }); - } -} - -export class DiscordPlayerQueryResultCache { - public expireAfter = DEFAULT_EXPIRY_TIMEOUT; - public constructor(public data: T, expireAfter: number = DEFAULT_EXPIRY_TIMEOUT) { - if (typeof expireAfter === 'number') { - this.expireAfter = Date.now() + expireAfter; - } - } - - public hasExpired() { - if (typeof this.expireAfter !== 'number' || isNaN(this.expireAfter) || this.expireAfter < 1) return false; - return Date.now() <= this.expireAfter; - } -} - -export interface QueryCacheResolverContext { - query: string; - requestedBy?: User; - queryType?: SearchQueryType | `ext:${string}`; -} diff --git a/packages/discord-player/src/utils/QueryResolver.ts b/packages/discord-player/src/utils/QueryResolver.ts deleted file mode 100644 index e081bbce6b..0000000000 --- a/packages/discord-player/src/utils/QueryResolver.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { QueryType } from '../types/types'; -import { TypeUtil } from './TypeUtil'; -import { Exceptions } from '../errors'; -import { fetch } from 'undici'; - -// #region scary things below *sigh* -const spotifySongRegex = /^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:track\/|\?uri=spotify:track:)((\w|-){22})(\?si=.+)?$/; -const spotifyPlaylistRegex = /^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})(\?si=.+)?$/; -const spotifyAlbumRegex = /^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:album\/|\?uri=spotify:album:)((\w|-){22})(\?si=.+)?$/; -const vimeoRegex = /^(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)$/; -const reverbnationRegex = /^https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)$/; -const attachmentRegex = /^https?:\/\/.+$/; -const appleMusicSongRegex = /^https?:\/\/music\.apple\.com\/.+?\/(song|album)\/.+?(\/.+?\?i=|\/)([0-9]+)$/; -const appleMusicPlaylistRegex = /^https?:\/\/music\.apple\.com\/.+?\/playlist\/.+\/pl\.(u-|pm-)?[a-zA-Z0-9]+$/; -const appleMusicAlbumRegex = /^https?:\/\/music\.apple\.com\/.+?\/album\/.+\/([0-9]+)$/; -const soundcloudTrackRegex = /^https?:\/\/(m.|www.)?soundcloud.com\/(\w|-)+\/(\w|-)+(.+)?$/; -const soundcloudPlaylistRegex = /^https?:\/\/(m.|www.)?soundcloud.com\/(\w|-)+\/sets\/(\w|-)+(.+)?$/; -const youtubePlaylistRegex = /^https?:\/\/(www.)?youtube.com\/playlist\?list=((PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{16,41})$/; -const youtubeVideoURLRegex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/; -const youtubeVideoIdRegex = /^[a-zA-Z0-9-_]{11}$/; -// #endregion scary things above *sigh* - -const DomainsMap = { - YouTube: ['youtube.com', 'youtu.be', 'music.youtube.com', 'gaming.youtube.com', 'www.youtube.com', 'm.youtube.com'], - Spotify: ['open.spotify.com', 'embed.spotify.com'], - Vimeo: ['vimeo.com', 'player.vimeo.com'], - ReverbNation: ['reverbnation.com'], - SoundCloud: ['soundcloud.com'], - AppleMusic: ['music.apple.com'] -}; - -// prettier-ignore -const redirectDomains = new Set([ - /^https?:\/\/spotify.link\/[A-Za-z0-9]+$/, -]); - -export interface ResolvedQuery { - type: (typeof QueryType)[keyof typeof QueryType]; - query: string; -} - -class QueryResolver { - /** - * Query resolver - */ - private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function - - static get regex() { - return { - spotifyAlbumRegex, - spotifyPlaylistRegex, - spotifySongRegex, - vimeoRegex, - reverbnationRegex, - attachmentRegex, - appleMusicAlbumRegex, - appleMusicPlaylistRegex, - appleMusicSongRegex, - soundcloudTrackRegex, - soundcloudPlaylistRegex, - youtubePlaylistRegex - }; - } - - /** - * Pre-resolve redirect urls - */ - static async preResolve(query: string, maxDepth = 5): Promise { - if (!TypeUtil.isString(query)) throw Exceptions.ERR_INVALID_ARG_TYPE(query, 'string', typeof query); - - for (const domain of redirectDomains) { - if (domain.test(query)) { - try { - const res = await fetch(query, { - method: 'GET', - redirect: 'follow' - }); - - if (!res.ok) break; - - // spotify does not "redirect", it returns a page with js that redirects - if (/^https?:\/\/spotify.app.link\/(.+)$/.test(res.url)) { - const body = await res.text(); - const target = body.split('https://open.spotify.com/track/')[1].split('?si=')[0]; - - if (!target) break; - - return `https://open.spotify.com/track/${target}`; - } - return maxDepth < 1 ? res.url : this.preResolve(res.url, maxDepth - 1); - } catch { - break; - } - } - } - - return query; - } - - /** - * Resolves the given search query - * @param {string} query The query - */ - static resolve(query: string, fallbackSearchEngine: (typeof QueryType)[keyof typeof QueryType] = QueryType.AUTO_SEARCH): ResolvedQuery { - if (!TypeUtil.isString(query)) throw Exceptions.ERR_INVALID_ARG_TYPE(query, 'string', typeof query); - if (!query.length) throw Exceptions.ERR_INFO_REQUIRED('query', String(query)); - - const resolver = (type: typeof fallbackSearchEngine, query: string) => ({ type, query }); - - try { - const url = new URL(query); - - if (DomainsMap.YouTube.includes(url.host)) { - query = query.replace(/(m(usic)?|gaming)\./, '').trim(); - const playlistId = url.searchParams.get('list'); - const videoId = url.searchParams.get('v'); - if (playlistId) { - if (videoId && playlistId.startsWith('RD')) return resolver(QueryType.YOUTUBE_PLAYLIST, `https://www.youtube.com/watch?v=${videoId}&list=${playlistId}`); - return resolver(QueryType.YOUTUBE_PLAYLIST, `https://www.youtube.com/playlist?list=${playlistId}`); - } - if (QueryResolver.validateId(query) || QueryResolver.validateURL(query)) return resolver(QueryType.YOUTUBE_VIDEO, query); - return resolver(fallbackSearchEngine, query); - } else if (DomainsMap.Spotify.includes(url.host)) { - query = query.replace(/intl-([a-zA-Z]+)\//, ''); - if (spotifyPlaylistRegex.test(query)) return resolver(QueryType.SPOTIFY_PLAYLIST, query); - if (spotifyAlbumRegex.test(query)) return resolver(QueryType.SPOTIFY_ALBUM, query); - if (spotifySongRegex.test(query)) return resolver(QueryType.SPOTIFY_SONG, query); - return resolver(fallbackSearchEngine, query); - } else if (DomainsMap.Vimeo.includes(url.host)) { - if (vimeoRegex.test(query)) return resolver(QueryType.VIMEO, query); - return resolver(fallbackSearchEngine, query); - } else if (DomainsMap.ReverbNation.includes(url.host)) { - if (reverbnationRegex.test(query)) return resolver(QueryType.REVERBNATION, query); - return resolver(fallbackSearchEngine, query); - } else if (DomainsMap.SoundCloud.includes(url.host)) { - if (soundcloudPlaylistRegex.test(query)) return resolver(QueryType.SOUNDCLOUD_PLAYLIST, query); - if (soundcloudTrackRegex.test(query)) return resolver(QueryType.SOUNDCLOUD_TRACK, query); - return resolver(fallbackSearchEngine, query); - } else if (DomainsMap.AppleMusic.includes(url.host)) { - if (appleMusicAlbumRegex.test(query)) return resolver(QueryType.APPLE_MUSIC_ALBUM, query); - if (appleMusicPlaylistRegex.test(query)) return resolver(QueryType.APPLE_MUSIC_PLAYLIST, query); - if (appleMusicSongRegex.test(query)) return resolver(QueryType.APPLE_MUSIC_SONG, query); - return resolver(fallbackSearchEngine, query); - } else { - return resolver(QueryType.ARBITRARY, query); - } - } catch { - return resolver(fallbackSearchEngine, query); - } - } - - /** - * Parses vimeo id from url - * @param {string} query The query - * @returns {string} - */ - static getVimeoID(query: string): string | null | undefined { - return QueryResolver.resolve(query).type === QueryType.VIMEO ? query.split('/').filter(Boolean).pop() : null; - } - - static validateId(q: string) { - return youtubeVideoIdRegex.test(q); - } - - static validateURL(q: string) { - return youtubeVideoURLRegex.test(q); - } -} - -export { QueryResolver }; diff --git a/packages/discord-player/src/utils/SequentialBucket.ts b/packages/discord-player/src/utils/SequentialBucket.ts deleted file mode 100644 index c717f36380..0000000000 --- a/packages/discord-player/src/utils/SequentialBucket.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { setTimeout } from 'timers/promises'; -import { AsyncQueue } from './AsyncQueue'; - -export type RequestEntity = () => Promise; - -export class SequentialBucket { - public limit = 1; - public remaining = 1; - public resetAfter = 0; - public queue = new AsyncQueue(); - public MAX_RETRIES = 5; - - /** - * Checks if the bucket is rate limited. - */ - public isRateLimited() { - return this.remaining <= 0 && Date.now() < this.resetAfter; - } - - /** - * Enqueues a request. - * @param req The request function to enqueue - */ - public async enqueue(req: RequestEntity) { - const entry = this.queue.acquire(); - await entry.getTask(); - - try { - return this._request(req); - } finally { - entry.release(); - } - } - - private async _request(req: RequestEntity, retries = 0): Promise { - while (this.isRateLimited()) { - const reset = this.resetAfter - Date.now(); - await setTimeout(reset); - } - - let pass = false; - - try { - const res = await req(); - - this._patchHeaders(res); - - if (res.status === 429) { - const reset = this.resetAfter - Date.now(); - await setTimeout(reset); - return this._request(req); - } - - if (!res.ok) { - let err: Error; - - try { - const body: { - code: number; - name: string; - message: string; - } = await res.json(); - - const error = new Error(body.message) as Error & { code: number }; - - error.name = body.name; - error.code = body.code; - - err = error; - } catch { - err = new Error(`HTTP Error: ${res.status} ${res.statusText}`); - } - - pass = true; - - throw err; - } - - return res; - } catch (e) { - if (pass) throw e; - - const badReq = e instanceof Error && /Error: 4[0-9]{2}/.test(e.message); - - if (!badReq && retries < this.MAX_RETRIES) { - return this._request(req, ++retries); - } - - throw e; - } - } - - private _patchHeaders(res: Response) { - const limit = Number(res.headers.get('X-RateLimit-Limit')); - const remaining = Number(res.headers.get('X-RateLimit-Remaining')); - const resetAfter = Number(res.headers.get('X-RateLimit-Reset')) * 1000 + Date.now(); - - if (!Number.isNaN(limit)) this.limit = limit; - if (!Number.isNaN(remaining)) this.remaining = remaining; - if (!Number.isNaN(resetAfter)) this.resetAfter = resetAfter; - } -} diff --git a/packages/discord-player/src/utils/TypeUtil.ts b/packages/discord-player/src/utils/TypeUtil.ts deleted file mode 100644 index b69e551f79..0000000000 --- a/packages/discord-player/src/utils/TypeUtil.ts +++ /dev/null @@ -1,34 +0,0 @@ -export class TypeUtil { - private constructor() { - return TypeUtil; - } - - // eslint-disable-next-line @typescript-eslint/ban-types - public static isFunction(t: unknown): t is Function { - return typeof t === 'function'; - } - - public static isNumber(t: unknown): t is number { - return typeof t === 'number' && !isNaN(t); - } - - public static isString(t: unknown): t is string { - return typeof t === 'string'; - } - - public static isBoolean(t: unknown): t is boolean { - return typeof t === 'boolean'; - } - - public static isNullish(t: unknown): t is null | undefined { - return t == null; - } - - public static isArray(t: unknown): t is unknown[] { - return Array.isArray(t); - } - - public static isError(t: unknown): t is Error { - return t instanceof Error; - } -} diff --git a/packages/discord-player/src/utils/Util.ts b/packages/discord-player/src/utils/Util.ts deleted file mode 100644 index fbaf17c2b8..0000000000 --- a/packages/discord-player/src/utils/Util.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { StageChannel, VoiceChannel } from 'discord.js'; -import { TimeData } from '../types/types'; -import { setTimeout } from 'node:timers/promises'; -import { GuildQueue } from '../queue'; -import { Playlist, Track } from '../fabric'; -import { Exceptions } from '../errors'; -import { randomInt } from 'node:crypto'; -import { - createFilter, - createSpotifyFilter, - fixTrackSuffix, - removeLive, - removeRemastered, - youtube, - removeZeroWidth, - replaceNbsp, - replaceSmartQuotes, - removeCleanExplicit -} from '@web-scrobbler/metadata-filter'; -import { TrackSource } from '../types/types'; - -export type RuntimeType = 'node' | 'deno' | 'bun' | 'unknown'; - -export interface Runtime { - name: RuntimeType; - version: string; -} - -class Util { - /** - * Utils - */ - private constructor() {} // eslint-disable-line @typescript-eslint/no-empty-function - - /** - * Gets the runtime information - */ - static getRuntime(): Runtime { - const version = typeof navigator !== 'undefined' ? navigator.userAgent : null; - - // @ts-ignore - if (typeof Deno !== 'undefined' && Deno.version) return { name: 'deno', version: Deno.version.deno }; - - // @ts-ignore - if (typeof Bun !== 'undefined' && Bun.version) return { name: 'bun', version: Bun.version }; - - if (typeof process !== 'undefined' && process.version) return { name: 'node', version: process.version }; - - return { name: 'unknown', version: version ?? 'unknown' }; - } - - /** - * Creates duration string - * @param {object} durObj The duration object - * @returns {string} - */ - static durationString(durObj: Record) { - return Object.values(durObj) - .map((m) => (isNaN(m) ? 0 : m)) - .join(':'); - } - - /** - * Parses milliseconds to consumable time object - * @param {number} milliseconds The time in ms - * @returns {TimeData} - */ - static parseMS(milliseconds: number) { - if (isNaN(milliseconds)) milliseconds = 0; - const round = milliseconds > 0 ? Math.floor : Math.ceil; - - return { - days: round(milliseconds / 86400000), - hours: round(milliseconds / 3600000) % 24, - minutes: round(milliseconds / 60000) % 60, - seconds: round(milliseconds / 1000) % 60 - } as TimeData; - } - - /** - * Builds time code - * @param {TimeData} duration The duration object - * @returns {string} - */ - static buildTimeCode(duration: TimeData) { - const items = Object.keys(duration); - const required = ['days', 'hours', 'minutes', 'seconds']; - - const parsed = items.filter((x) => required.includes(x)).map((m) => duration[m as keyof TimeData]); - const final = parsed - .slice(parsed.findIndex((x) => x !== 0)) - .map((x) => x.toString().padStart(2, '0')) - .join(':'); - - return final.length <= 3 ? `0:${final.padStart(2, '0') || 0}` : final; - } - - /** - * Formats duration - * @param {number} duration The duration in ms - */ - static formatDuration(duration: number) { - return this.buildTimeCode(this.parseMS(duration)); - } - - /** - * Picks last item of the given array - * @param {any[]} arr The array - * @returns {any} - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static last(arr: T[]): T { - if (!Array.isArray(arr)) return arr; - return arr[arr.length - 1]; - } - - /** - * Checks if the voice channel is empty - * @param {VoiceChannel|StageChannel} channel The voice channel - * @returns {boolean} - */ - static isVoiceEmpty(channel: VoiceChannel | StageChannel) { - return channel && channel.members.filter((member) => !member.user.bot).size === 0; - } - - /** - * Cleans the track title - * @param title The title - * @param source The source - * @returns Cleaned title - */ - static cleanTitle(title: string, source: TrackSource) { - try { - const filterOpts = { - // prettier-ignore - track: [ - removeRemastered, - removeLive, - fixTrackSuffix, - removeZeroWidth, - replaceNbsp, - replaceSmartQuotes, - removeCleanExplicit - ] - }; - const spotifyFilter = createFilter(filterOpts); - spotifyFilter.extend(createSpotifyFilter()); - const defaultFilter = createFilter(filterOpts); - - switch (source) { - case 'youtube': - return youtube(title); - case 'spotify': - return spotifyFilter.filterField('track', title); - default: - return defaultFilter.filterField('track', title); - } - } catch { - return title; - } - } - - /** - * Safer require - * @param {string} id Node require id - * @returns {any} - */ - static require(id: string) { - try { - return { module: require(id), error: null }; - } catch (error) { - return { module: null, error }; - } - } - - static async import(id: string) { - try { - const mod = await import(id); - return { module: mod, error: null }; - } catch (error) { - return { module: null, error }; - } - } - - /** - * Asynchronous timeout - * @param {number} time The time in ms to wait - * @returns {Promise} - */ - static wait(time: number) { - return setTimeout(time, undefined, { ref: false }); - } - - static noop() {} // eslint-disable-line @typescript-eslint/no-empty-function - - static async getFetch() { - if ('fetch' in globalThis) return globalThis.fetch; - for (const lib of ['node-fetch', 'undici']) { - try { - return await import(lib).then((res) => res.fetch || res.default?.fetch || res.default); - } catch { - try { - // eslint-disable-next-line - const res = require(lib); - if (res) return res.fetch || res.default?.fetch || res.default; - } catch { - // no? - } - } - } - } - - static warn(message: string, code = 'DeprecationWarning', detail?: string) { - process.emitWarning(message, { - code, - detail - }); - } - - static randomChoice(src: T[]): T { - return src[randomInt(src.length)]; - } - - static arrayCloneShuffle(src: T[]): T[] { - const arr = src.slice(); - - let m = arr.length; - - while (m) { - const i = Math.floor(Math.random() * m--); - [arr[m], arr[i]] = [arr[i], arr[m]]; - } - - return arr; - } -} - -export const VALIDATE_QUEUE_CAP = (queue: GuildQueue, items: Playlist | Track | Track[]) => { - const tracks = items instanceof Playlist ? items.tracks : Array.isArray(items) ? items : [items]; - - if (queue.maxSize < 1 || queue.maxSize === Infinity) return; - - const maxCap = queue.getCapacity(); - - if (maxCap < tracks.length) { - throw Exceptions.ERR_OUT_OF_SPACE('tracks queue', maxCap, tracks.length); - } -}; - -export { Util }; diff --git a/packages/discord-player/src/utils/__internal__/_container.ts b/packages/discord-player/src/utils/__internal__/_container.ts deleted file mode 100644 index 037577bd36..0000000000 --- a/packages/discord-player/src/utils/__internal__/_container.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Player } from '../../Player'; -import { Collection } from '@discord-player/utils'; - -export const instances = new Collection(); -export const globalRegistry = new Collection(); diff --git a/packages/discord-player/src/utils/__internal__/addPlayer.ts b/packages/discord-player/src/utils/__internal__/addPlayer.ts deleted file mode 100644 index 4d05a6bdde..0000000000 --- a/packages/discord-player/src/utils/__internal__/addPlayer.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Player } from '../../Player'; -import { instances } from './_container'; - -export function addPlayer(player: Player) { - if (instances.has(player.id)) return true; - - instances.set(player.id, player); - - return instances.has(player.id); -} diff --git a/packages/discord-player/src/utils/__internal__/clearPlayer.ts b/packages/discord-player/src/utils/__internal__/clearPlayer.ts deleted file mode 100644 index 1451fcf4fc..0000000000 --- a/packages/discord-player/src/utils/__internal__/clearPlayer.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Player } from '../../Player'; -import { instances } from './_container'; - -export function clearPlayer(player: Player) { - return instances.delete(player.id); -} diff --git a/packages/discord-player/src/utils/__internal__/getGlobalRegistry.ts b/packages/discord-player/src/utils/__internal__/getGlobalRegistry.ts deleted file mode 100644 index 8059691d41..0000000000 --- a/packages/discord-player/src/utils/__internal__/getGlobalRegistry.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { globalRegistry } from './_container'; - -export function getGlobalRegistry() { - return globalRegistry; -} diff --git a/packages/discord-player/src/utils/__internal__/getPlayers.ts b/packages/discord-player/src/utils/__internal__/getPlayers.ts deleted file mode 100644 index 8997bd611f..0000000000 --- a/packages/discord-player/src/utils/__internal__/getPlayers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { instances } from './_container'; - -export function getPlayers() { - return instances.array(); -} diff --git a/packages/discord-player/src/utils/__internal__/index.ts b/packages/discord-player/src/utils/__internal__/index.ts deleted file mode 100644 index 87fc9836fa..0000000000 --- a/packages/discord-player/src/utils/__internal__/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './_container'; -export * from './addPlayer'; -export * from './clearPlayer'; -export * from './getPlayers'; -export * from './getGlobalRegistry'; diff --git a/packages/discord-player/src/utils/serde.ts b/packages/discord-player/src/utils/serde.ts deleted file mode 100644 index 702330b0a2..0000000000 --- a/packages/discord-player/src/utils/serde.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { Exceptions } from '../errors'; -import { Playlist, type SerializedTrack, Track, SerializedPlaylist } from '../fabric'; -import { TypeUtil } from './TypeUtil'; -import { Buffer } from 'buffer'; -import { Player } from '../Player'; - -export enum SerializedType { - Track = 'track', - Playlist = 'playlist' -} - -export type Encodable = SerializedTrack | SerializedPlaylist; - -const isTrack = (data: any): data is SerializedTrack => data.$type === SerializedType.Track; -const isPlaylist = (data: any): data is SerializedPlaylist => data.$type === SerializedType.Playlist; - -export function serialize(data: Track | Playlist | any) { - if (data instanceof Track) return data.serialize(); - if (data instanceof Playlist) return data.serialize(); - - try { - return data.toJSON(); - } catch { - throw Exceptions.ERR_SERIALIZATION_FAILED(); - } -} - -export function deserialize(player: Player, data: Encodable) { - if (isTrack(data)) return Track.fromSerialized(player, data); - if (isPlaylist(data)) return Playlist.fromSerialized(player, data); - - throw Exceptions.ERR_DESERIALIZATION_FAILED(); -} - -export function encode(data: Encodable) { - const str = JSON.stringify(data); - - return Buffer.from(str).toString('base64'); -} - -export function decode(data: string) { - const str = Buffer.from(data, 'base64').toString(); - - return JSON.parse(str); -} - -export function tryIntoThumbnailString(data: any) { - if (!data) return null; - try { - if (TypeUtil.isString(data)) return data; - return data?.url ?? data?.thumbnail?.url ?? null; - } catch { - return null; - } -} diff --git a/apps/music-bot/src/recordings/.gitkeep b/packages/discord-player/src/voip/connection.ts similarity index 100% rename from apps/music-bot/src/recordings/.gitkeep rename to packages/discord-player/src/voip/connection.ts diff --git a/packages/discord-player/src/voip/constants.ts b/packages/discord-player/src/voip/constants.ts new file mode 100644 index 0000000000..eae6ee1630 --- /dev/null +++ b/packages/discord-player/src/voip/constants.ts @@ -0,0 +1,29 @@ +export const NONCE = Buffer.alloc(24); + +export const OPUS_SAMPLE_RATE = 48000; + +export const OPUS_CHANNELS = 2; + +export const OPUS_FRAME_SIZE = 960; + +export const OPUS_FRAME_DURATION = Math.floor(OPUS_FRAME_SIZE / OPUS_SAMPLE_RATE); + +export const OPUS_SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]); + +export const TIMESTAMP_INC = (OPUS_SAMPLE_RATE / 100) * OPUS_CHANNELS; + +export const MAX_COUNTER_VALUE = 2 ** 32 - 1; + +export const UDP_KEEPALIVE_INTERVAL = 5e3; + +export const MAX_NONCE = 2 ** 32 - 1; + +export const EncryptionMode = { + XSALSA20_POLY1305_LITE: 'xsalsa20_poly1305_lite', + XSALSA20_POLY1305_SUFFIX: 'xsalsa20_poly1305_suffix', + XSALSA20_POLY1305: 'xsalsa20_poly1305' +} as const; + +export type EncryptionMode = (typeof EncryptionMode)[keyof typeof EncryptionMode]; + +export const ENCRYPTION_MODES = new Set(['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'] as const); diff --git a/packages/discord-player/src/voip/libsodium.ts b/packages/discord-player/src/voip/libsodium.ts new file mode 100644 index 0000000000..c34401d67b --- /dev/null +++ b/packages/discord-player/src/voip/libsodium.ts @@ -0,0 +1,80 @@ +import { unsafe } from '../common/types'; + +export interface ISodium { + open(buffer: Buffer, nonce: Buffer, key: Uint8Array): Buffer; + close(buffer: Buffer, nonce: Buffer, key: Uint8Array): Buffer; + random(length: number, target?: Buffer): Buffer; +} + +const libs: Record ISodium> = { + 'sodium-native': (lib) => ({ + open(buffer, nonce, key) { + const result = Buffer.allocUnsafe(buffer.length - lib.crypto_secretbox_MACBYTES); + lib.crypto_secretbox_open_easy(result, buffer, nonce, key); + return result; + }, + close(buffer, nonce, key) { + const result = Buffer.allocUnsafe(buffer.length + lib.crypto_secretbox_MACBYTES); + lib.crypto_secretbox_easy(result, buffer, nonce, key); + return result; + }, + random(length, target) { + const buffer = target ?? Buffer.allocUnsafe(length); + lib.randombytes_buf(buffer); + return buffer; + } + }), + sodium: (lib) => ({ + open: lib.api.crypto_secretbox_open_easy, + close: lib.api.crypto_secretbox_easy, + random: (length, target) => { + const buffer = target ?? Buffer.allocUnsafe(length); + lib.api.randombytes_buf(buffer); + return buffer; + } + }), + 'libsodium-wrappers': (lib) => ({ + open: lib.crypto_secretbox_open_easy, + close: lib.crypto_secretbox_easy, + random: lib.randombytes_buf + }), + tweetnacl: (lib) => ({ + open: lib.secretbox.open, + close: lib.secretbox, + random: lib.randombytes_buf + }) +}; + +libs['sodium-javascript'] = libs['sodium-native']; + +const err = () => { + const supported = Object.keys(libs).join(', '); + + throw new Error(`No supported libsodium found. Make sure you have installed one of ${supported} in your project.`); +}; + +const libsodium: ISodium = { + open: err, + close: err, + random: err +}; + +(async () => { + const libsEntries = Object.entries(libs); + + for (const [name, lib] of libsEntries) { + try { + const _sod = await import(name); + const sod = 'default' in _sod ? _sod.default : _sod; + + if (name === 'libsodium-wrappers' && 'ready' in sod) await sod.ready; + + Object.assign(libsodium, lib(sod)); + break; + } catch { + // + } + } +})(); + +export { libsodium }; diff --git a/packages/discord-player/src/voip/networking.ts b/packages/discord-player/src/voip/networking.ts new file mode 100644 index 0000000000..e7841e24f2 --- /dev/null +++ b/packages/discord-player/src/voip/networking.ts @@ -0,0 +1,65 @@ +import { EventEmitter } from '../common/EventEmitter'; +import { EncryptionMode, MAX_NONCE, NONCE } from './constants'; +import { libsodium } from './libsodium'; + +export const VoipEvents = {} as const; + +export type VoipEvents = (typeof VoipEvents)[keyof typeof VoipEvents]; + +export interface VoipEventsMap {} + +export interface NetworkingOptions {} + +export interface ConnectionData { + encryptionMode: EncryptionMode; + nonce: number; + nonceBuffer: Buffer; + packetsPlayed: number; + secretKey: Uint8Array; + sequence: number; + speaking: boolean; + ssrc: number; + timestamp: number; +} + +export class Networking extends EventEmitter { + public constructor(public readonly options: NetworkingOptions) { + super(); + } + + #createAudioPacket(opus: Buffer, data: ConnectionData) { + const packetBuffer = Buffer.alloc(12); + packetBuffer[0] = 0x80; + packetBuffer[1] = 0x78; + + const { sequence, timestamp, ssrc } = data; + + packetBuffer.writeUIntBE(sequence, 2, 2); + packetBuffer.writeUIntBE(timestamp, 4, 4); + packetBuffer.writeUIntBE(ssrc, 8, 4); + + packetBuffer.copy(NONCE, 0, 0, 12); + + return Buffer.concat([packetBuffer, ...this.#encrypt(opus, data)]); + } + + #encrypt(opus: Buffer, data: ConnectionData) { + switch (data.encryptionMode) { + case EncryptionMode.XSALSA20_POLY1305_LITE: { + data.nonce++; + if (data.nonce > MAX_NONCE) data.nonce = 0; + data.nonceBuffer.writeUInt32BE(data.nonce, 0); + + return [libsodium.close(opus, data.nonceBuffer, data.secretKey)]; + } + case EncryptionMode.XSALSA20_POLY1305_SUFFIX: { + const random = libsodium.random(24, data.nonceBuffer); + + return [libsodium.close(opus, random, data.secretKey), random]; + } + default: { + return [libsodium.close(opus, data.nonceBuffer, data.secretKey)]; + } + } + } +} diff --git a/packages/discord-player/src/voip/udp.ts b/packages/discord-player/src/voip/udp.ts new file mode 100644 index 0000000000..eb0e38560c --- /dev/null +++ b/packages/discord-player/src/voip/udp.ts @@ -0,0 +1,134 @@ +import { isIPv4 } from 'node:net'; +import { createSocket, type Socket } from 'node:dgram'; +import { EventEmitter } from '../common/EventEmitter'; +import { MAX_COUNTER_VALUE, UDP_KEEPALIVE_INTERVAL } from './constants'; + +export interface RemoteAddress { + address: string; + port: number; +} + +export interface VoipUdpOptions { + address: string; +} + +export const VoipUdpEvents = { + Error: 'error', + Close: 'close', + Message: 'message' +} as const; + +export type VoipUdpEvents = (typeof VoipUdpEvents)[keyof typeof VoipUdpEvents]; + +export interface VoipUdpEventsMap { + [VoipUdpEvents.Error]: (error: Error) => void; + [VoipUdpEvents.Close]: () => void; + [VoipUdpEvents.Message]: (data: Buffer) => void; +} + +export class VoipUdpSocket extends EventEmitter { + private readonly socket: Socket; + private keepAliveBuffer: Buffer; + private keepAliveInterval: NodeJS.Timeout; + private keepAliveCounter = 0; + + public constructor(private readonly remoteAddress: RemoteAddress) { + super(); + + this.socket = createSocket('udp4'); + this.socket.on('error', this.#onError.bind(this)); + this.socket.on('message', this.#onMessage.bind(this)); + this.socket.on('close', this.#onClose.bind(this)); + + this.keepAliveBuffer = Buffer.alloc(0); + this.keepAliveInterval = setInterval(this.#keepAlive.bind(this), UDP_KEEPALIVE_INTERVAL); + + setImmediate(() => this.#keepAlive()); + } + + public destroy() { + try { + this.debug?.('Destroying the UDP socket'); + this.socket.close(); + } catch { + // + } + + clearInterval(this.keepAliveInterval); + } + + public send(buffer: Buffer) { + const { address, port } = this.remoteAddress; + + this.socket.send(buffer, port, address); + } + + public performIPDiscovery(ssrc: number): Promise { + return new Promise((resolve, reject) => { + const listener = (message: Buffer) => { + try { + if (message.readUInt16BE(0) !== 2) return; + + const data = this.#parseLocalPacket(message); + this.socket.off('message', listener); + this.socket.off('close', rejectListener); + resolve(data); + } catch { + // + } + }; + + const rejectListener = () => reject(new Error('Socket closed before performing IP discovery')); + + this.socket.on('message', listener); + this.socket.once('close', rejectListener); + + const buffer = Buffer.alloc(74); + + buffer.writeUInt16BE(1, 0); + buffer.writeUInt16BE(70, 2); + buffer.writeUInt32BE(ssrc, 4); + + this.send(buffer); + }); + } + + #parseLocalPacket(buffer: Buffer): RemoteAddress { + const msg = Buffer.from(buffer); + + const address = msg.subarray(8, msg.indexOf(0, 8)).toString('utf-8'); + + if (!isIPv4(address)) { + throw new Error('Invalid IP address received'); + } + + const port = msg.readUInt16BE(msg.length - 2); + + return { + address, + port + } satisfies RemoteAddress; + } + + #onError(error: Error) { + this.emit(VoipUdpEvents.Error, error); + } + + #onClose() { + this.emit(VoipUdpEvents.Close); + } + + #onMessage(data: Buffer) { + this.emit(VoipUdpEvents.Message, data); + } + + #keepAlive() { + this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0); + this.send(this.keepAliveBuffer); + this.keepAliveCounter++; + + if (this.keepAliveCounter > MAX_COUNTER_VALUE) { + this.keepAliveCounter = 0; + } + } +} diff --git a/packages/discord-player/src/voip/websocket.ts b/packages/discord-player/src/voip/websocket.ts new file mode 100644 index 0000000000..b1ae283a83 --- /dev/null +++ b/packages/discord-player/src/voip/websocket.ts @@ -0,0 +1,117 @@ +import WebSocket from 'ws'; +import { EventEmitter } from '../common/EventEmitter'; +import type { unsafe } from '../common/types'; +import { VoiceOpcodes } from 'discord-api-types/voice/v4'; + +export interface VoipWebSocketOptions { + address: string; +} + +export const VoipWebSocketEvents = { + Error: 'error', + Open: 'open', + Close: 'close', + Packet: 'packet' +} as const; + +export type VoipWebSocketEvents = (typeof VoipWebSocketEvents)[keyof typeof VoipWebSocketEvents]; + +export interface VoipWebSocketEventsMap { + [VoipWebSocketEvents.Error]: (error: Error) => void; + [VoipWebSocketEvents.Open]: (event: WebSocket.Event) => void; + [VoipWebSocketEvents.Close]: (event: WebSocket.CloseEvent) => void; + [VoipWebSocketEvents.Packet]: (data: unsafe) => void; +} + +export class VoipWebSocket extends EventEmitter { + private readonly ws: WebSocket; + private lastHeartbeatSent = 0; + private missedHeartbeats = 0; + private heartbeatInterval: NodeJS.Timeout | undefined; + + public constructor(private readonly options: VoipWebSocketOptions) { + super(); + + this.ws = new WebSocket(options.address); + this.ws.onmessage = this.#onMessage.bind(this); + this.ws.onerror = this.#onError.bind(this); + this.ws.onopen = this.#onOpen.bind(this); + this.ws.onclose = this.#onClose.bind(this); + } + + public destroy() { + try { + this.debug?.('Destroying the WebSocket'); + this.setHeartbeat(0); + this.ws.close(1000); + } catch (error) { + this.emit(VoipWebSocketEvents.Error, error as Error); + } + } + + public send(data: unsafe) { + try { + const payload = JSON.stringify(data); + this.debug?.('Sending payload ${payload}'); + this.ws.send(payload); + } catch (error) { + this.emit(VoipWebSocketEvents.Error, error as Error); + } + } + + public setHeartbeat(interval: number) { + if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); + if (interval <= 0) return; + + this.heartbeatInterval = setInterval(() => { + if (this.lastHeartbeatSent !== 0 && this.missedHeartbeats >= 3) { + this.debug?.('Missed too many heartbeats'); + this.ws.close(); + this.setHeartbeat(0); + return; + } + + this.#heartbeat(); + }, interval); + } + + #heartbeat() { + this.lastHeartbeatSent = Date.now(); + this.missedHeartbeats++; + + this.send({ + op: VoiceOpcodes.Heartbeat, + d: this.lastHeartbeatSent + }); + } + + #onMessage(event: WebSocket.MessageEvent) { + if (typeof event.data !== 'string') return; + + try { + const data = JSON.parse(event.data); + + this.debug?.('Received payload ${event.data}'); + + if (data.op === VoiceOpcodes.HeartbeatAck) { + this.missedHeartbeats = 0; + } + + this.emit(VoipWebSocketEvents.Packet, data); + } catch (error) { + this.emit(VoipWebSocketEvents.Error, error as Error); + } + } + + #onError(event: WebSocket.ErrorEvent | Error) { + this.emit(VoipWebSocketEvents.Error, event instanceof Error ? event : event.error); + } + + #onOpen(event: WebSocket.Event) { + this.emit(VoipWebSocketEvents.Open, event); + } + + #onClose(event: WebSocket.CloseEvent) { + this.emit(VoipWebSocketEvents.Close, event); + } +} diff --git a/packages/downloader/LICENSE b/packages/downloader/LICENSE deleted file mode 100644 index fe07fc7364..0000000000 --- a/packages/downloader/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Androz2091 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/downloader/README.md b/packages/downloader/README.md deleted file mode 100644 index e8f7ff5209..0000000000 --- a/packages/downloader/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Downloader -Extractor for **[discord-player](https://npmjs.com/package/discord-player)** using **[youtube-dl](https://npmjs.com/package/youtube-dl)**. - -# Installing - -```sh -npm i @discord-player/downloader -``` - -# Example -## General -```js -const downloader = require("@discord-player/downloader").Downloader; -const fs = require("fs"); -const url = "https://soundcloud.com/dogesounds/alan-walker-feat-k-391-ignite"; - -const stream = downloader.download(url); -stream.pipe(fs.createWriteStream("./song.mp3")); -``` - -## With Discord Player -```js -const downloader = require("@discord-player/downloader").Downloader; - -player.use("YOUTUBE_DL", downloader); // enables youtube-dl extractor for discord-player -``` \ No newline at end of file diff --git a/packages/downloader/package.json b/packages/downloader/package.json deleted file mode 100644 index 28c0611b1b..0000000000 --- a/packages/downloader/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@discord-player/downloader", - "version": "3.0.2", - "description": "Stream extractor for discord-player via youtube-dl", - "keywords": [ - "discord-player", - "music", - "bot", - "discord.js", - "javascript", - "voip", - "lavalink", - "lavaplayer" - ], - "author": "Androz2091 ", - "homepage": "https://discord-player.js.org", - "license": "MIT", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Androz2091/discord-player.git" - }, - "scripts": { - "build": "tsup", - "build:check": "tsc --noEmit", - "lint": "eslint src --ext .ts --fix" - }, - "bugs": { - "url": "https://github.com/Androz2091/discord-player/issues" - }, - "devDependencies": { - "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "vitest": "^0.34.6" - }, - "dependencies": { - "youtube-dl-exec": "^2.1.11" - }, - "typedoc": { - "entryPoint": "./src/index.ts", - "readmeFile": "./README.md", - "tsconfig": "./tsconfig.json" - } -} diff --git a/packages/downloader/src/Downloader.ts b/packages/downloader/src/Downloader.ts deleted file mode 100644 index 621cb69bd8..0000000000 --- a/packages/downloader/src/Downloader.ts +++ /dev/null @@ -1,92 +0,0 @@ -import ytdl from 'youtube-dl-exec'; - -export interface Info { - title: string; - duration: number; - thumbnail: string; - views: number; - author: string; - description: string; - url: string; - source: string; - engine: import('stream').Readable; -} - -export class Downloader { - constructor() { - return Downloader; - } - - /** - * Downloads stream through youtube-dl - * @param {string} url URL to download stream from - */ - static download(url: string) { - if (!url || typeof url !== 'string') throw new Error('Invalid url'); - - const ytdlProcess = ytdl.exec(url, { - output: '-', - quiet: true, - preferFreeFormats: true, - limitRate: '100K' - }); - - if (!ytdlProcess.stdout) throw new Error('No stdout'); - const stream = ytdlProcess.stdout; - - stream.on('error', () => { - if (!ytdlProcess.killed) ytdlProcess.kill(); - stream.resume(); - }); - - return stream; - } - - /** - * Returns stream info - * @param {string} url stream url - */ - static getInfo(url: string) { - // eslint-disable-next-line - return new Promise<{ playlist: any; info: Info[] }>(async (resolve, reject) => { - if (!url || typeof url !== 'string') reject(new Error('Invalid url')); - - const info = await ytdl(url, { - dumpSingleJson: true, - skipDownload: true, - simulate: true - }).catch(() => undefined); - if (!info) return resolve({ playlist: null, info: [] }); - - try { - const data = { - title: info.fulltitle || info.title || 'Attachment', - duration: (info.duration || 0) * 1000, - thumbnail: info.thumbnails ? info.thumbnails[0].url : info.thumbnail || 'https://upload.wikimedia.org/wikipedia/commons/2/2a/ITunes_12.2_logo.png', - views: info.view_count || 0, - author: info.uploader || info.channel || 'YouTubeDL Media', - description: info.description || '', - url: url, - source: info.extractor, - get engine() { - return Downloader.download(url); - } - } as Info; - - resolve({ playlist: null, info: [data] }); - } catch { - resolve({ playlist: null, info: [] }); - } - }); - } - - static validate(url: string) { - const REGEX = - /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/; - return REGEX.test(url || ''); - } - - static get important() { - return true; - } -} diff --git a/packages/downloader/src/index.ts b/packages/downloader/src/index.ts deleted file mode 100644 index 4f98195ea1..0000000000 --- a/packages/downloader/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Downloader'; -export * as ytdl from 'youtube-dl-exec'; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/downloader/tsconfig.json b/packages/downloader/tsconfig.json deleted file mode 100644 index 3c774c914c..0000000000 --- a/packages/downloader/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "@discord-player/tsconfig/base.json", - "include": [ - "src/**/*" - ] -} \ No newline at end of file diff --git a/packages/downloader/tsup.config.ts b/packages/downloader/tsup.config.ts deleted file mode 100644 index e161ba4d4d..0000000000 --- a/packages/downloader/tsup.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '../../tsup.config'; -import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; - -export default defineConfig({ - esbuildPlugins: [esbuildPluginVersionInjector()] -}); diff --git a/packages/extractor/LICENSE b/packages/extractor/LICENSE deleted file mode 100644 index fe07fc7364..0000000000 --- a/packages/extractor/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Androz2091 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/extractor/README.md b/packages/extractor/README.md deleted file mode 100644 index b67a9d6c39..0000000000 --- a/packages/extractor/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Extractors - -Extractors for `discord-player`. - -# Example - -```js -const { YouTubeExtractor } = require('@discord-player/extractor'); -const player = useMainPlayer(); - -// enables youtube extractor -player.extractors.register(YouTubeExtractor); -``` - -# Available Extractors - -- Attachment (Remote, Local) -- Reverbnation -- SoundCloud -- Vimeo -- YouTube -- Spotify -- Apple Music - -# Lyrics - -```js -const { lyricsExtractor } = require('@discord-player/extractor'); -const lyricsClient = lyricsExtractor('api_key_or_leave_it_blank'); - -lyricsClient - .search('alan walker faded') - .then((x) => console.log(x)) - .catch(console.error); -``` - -## Response - -```js -{ - title: 'Faded', - id: 2396871, - thumbnail: 'https://images.genius.com/10db94c5c11e1bb1ac9cc917a6c59250.300x300x1.jpg', - image: 'https://images.genius.com/10db94c5c11e1bb1ac9cc917a6c59250.1000x1000x1.jpg', - url: 'https://genius.com/Alan-walker-faded-lyrics', - artist: { - name: 'Alan Walker', - id: 456537, - url: 'https://genius.com/artists/Alan-walker', - image: 'https://images.genius.com/5dc7f5c57981ba34e464414f7fc08ebf.1000x333x1.jpg' - }, - lyrics: '[Verse 1]\n' + - 'You were the shadow to my light\n' + - 'Did you feel us?\n' + - 'Another star, you fade away\n' + - 'Afraid our aim is out of sight\n' + - 'Wanna see us alight\n' + - '\n' + - '[Pre-Chorus 1]\n' + - 'Where are you now?\n' + - 'Where are you now?\n' + - 'Where are you now?\n' + - 'Was it all in my fantasy?\n' + - 'Where are you now?\n' + - 'Were you only imaginary?\n' + - '\n' + - '[Chorus]\n' + - 'Where are you now?\n' + - 'Atlantis, under the sea, under the sea\n' + - 'Where are you now? Another dream\n' + - "The monster's running wild inside of me\n" + - "I'm faded, I'm faded\n" + - "So lost, I'm faded, I'm faded\n" + - "So lost, I'm faded\n" + - '\n' + - '[Verse 2]\n' + - 'These shallow waters never met what I needed\n' + - "I'm letting go, a deeper dive\n" + - 'Eternal silence of the sea\n' + - "I'm breathing, alive\n" + - '\n' + - '[Pre-Chorus 2]\n' + - 'Where are you now?\n' + - 'Where are you now?\n' + - 'Under the bright but faded lights\n' + - 'You set my heart on fire\n' + - 'Where are you now?\n' + - 'Where are you now?\n' + - '\n' + - '[Chorus]\n' + - 'Where are you now?\n' + - 'Atlantis, under the sea, under the sea\n' + - 'Where are you now? Another dream\n' + - "The monster's running wild inside of me\n" + - "I'm faded, I'm faded\n" + - "So lost, I'm faded, I'm faded\n" + - "So lost, I'm faded" -} -``` diff --git a/packages/extractor/package.json b/packages/extractor/package.json deleted file mode 100644 index 3f0ad03e43..0000000000 --- a/packages/extractor/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "@discord-player/extractor", - "version": "4.4.7", - "description": "Extractors for discord-player", - "keywords": [ - "discord-player", - "music", - "bot", - "discord.js", - "javascript", - "voip", - "lavalink", - "lavaplayer" - ], - "author": "Androz2091 ", - "homepage": "https://discord-player.js.org", - "license": "MIT", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Androz2091/discord-player.git" - }, - "scripts": { - "build": "tsup", - "build:check": "tsc --noEmit", - "lint": "eslint src --ext .ts --fix" - }, - "bugs": { - "url": "https://github.com/Androz2091/discord-player/issues" - }, - "devDependencies": { - "@discord-player/tsconfig": "workspace:^", - "@distube/ytdl-core": "^4.13.0", - "@types/node": "^18.11.18", - "discord-player": "workspace:^", - "mediaplex": "^0.0.7", - "play-dl": "^1.9.6", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "vitest": "^0.34.6", - "youtube-ext": "^1.1.14", - "yt-stream": "^1.4.8", - "ytdl-core": "^4.11.4" - }, - "dependencies": { - "file-type": "^16.5.4", - "genius-lyrics": "^4.4.6", - "isomorphic-unfetch": "^4.0.2", - "node-html-parser": "^6.1.4", - "reverbnation-scraper": "^2.0.0", - "soundcloud.ts": "^0.5.2", - "spotify-url-info": "^3.2.6", - "youtube-sr": "^4.3.9" - }, - "typedoc": { - "entryPoint": "./src/index.ts", - "readmeFile": "./README.md", - "tsconfig": "./tsconfig.json" - } -} diff --git a/packages/extractor/src/extractors/AppleMusicExtractor.ts b/packages/extractor/src/extractors/AppleMusicExtractor.ts deleted file mode 100644 index e3732ef9fa..0000000000 --- a/packages/extractor/src/extractors/AppleMusicExtractor.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { ExtractorInfo, ExtractorSearchContext, ExtractorStreamable, GuildQueueHistory, Playlist, QueryType, SearchQueryType, Track, Util } from 'discord-player'; -import { AppleMusic } from '../internal'; -import { Readable } from 'stream'; -import { StreamFN, pullYTMetadata } from './common/helper'; -import { BridgeProvider } from './common/BridgeProvider'; -import { BridgedExtractor } from './BridgedExtractor'; - -export interface AppleMusicExtractorInit { - createStream?: (ext: AppleMusicExtractor, url: string) => Promise; - bridgeProvider?: BridgeProvider; -} - -export class AppleMusicExtractor extends BridgedExtractor { - public static identifier = 'com.discord-player.applemusicextractor' as const; - private _stream!: StreamFN; - - public async activate(): Promise { - this.protocols = ['amsearch', 'applemusic']; - const fn = this.options.createStream; - - if (typeof fn === 'function') { - this._stream = (q: string) => { - return fn(this, q); - }; - } - } - - public async deactivate() { - this.protocols = []; - } - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - // prettier-ignore - return ([ - QueryType.APPLE_MUSIC_ALBUM, - QueryType.APPLE_MUSIC_PLAYLIST, - QueryType.APPLE_MUSIC_SONG, - QueryType.APPLE_MUSIC_SEARCH, - QueryType.AUTO, - QueryType.AUTO_SEARCH - ]).some((t) => t === type); - } - - public async getRelatedTracks(track: Track, history: GuildQueueHistory) { - if (track.queryType === QueryType.APPLE_MUSIC_SONG) { - const data = await this.handle(track.author || track.title, { - type: QueryType.APPLE_MUSIC_SEARCH, - requestedBy: track.requestedBy - }); - - const unique = data.tracks.filter((t) => !history.tracks.some((h) => h.url === t.url)); - return unique.length > 0 ? this.createResponse(null, unique) : this.createResponse(); - } - - return this.createResponse(); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - if (context.protocol === 'amsearch') context.type = QueryType.APPLE_MUSIC_SEARCH; - - switch (context.type) { - case QueryType.AUTO: - case QueryType.AUTO_SEARCH: - case QueryType.APPLE_MUSIC_SEARCH: { - const data = await AppleMusic.search(query); - if (!data || !data.length) return this.createResponse(); - const tracks = data.map( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (m: any) => { - const track: Track = new Track(this.context.player, { - author: m.artist.name, - description: m.title, - duration: typeof m.duration === 'number' ? Util.buildTimeCode(Util.parseMS(m.duration)) : m.duration, - thumbnail: m.thumbnail, - title: m.title, - url: m.url, - views: 0, - source: 'apple_music', - requestedBy: context.requestedBy, - queryType: 'appleMusicSong', - metadata: { - source: m, - bridge: null - }, - requestMetadata: async () => { - return { - source: m, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, track)).data : await pullYTMetadata(this, track) - }; - } - }); - - track.extractor = this; - - return track; - } - ); - - return this.createResponse(null, tracks); - } - case QueryType.APPLE_MUSIC_ALBUM: { - const info = await AppleMusic.getAlbumInfo(query); - if (!info) return this.createResponse(); - - const playlist = new Playlist(this.context.player, { - author: { - name: info.artist.name, - url: '' - }, - description: info.title, - id: info.id, - source: 'apple_music', - thumbnail: info.thumbnail, - title: info.title, - tracks: [], - type: 'album', - url: info.url, - rawPlaylist: info - }); - - playlist.tracks = info.tracks.map( - ( - m: any // eslint-disable-line - ) => { - const track: Track = new Track(this.context.player, { - author: m.artist.name, - description: m.title, - duration: typeof m.duration === 'number' ? Util.buildTimeCode(Util.parseMS(m.duration)) : m.duration, - thumbnail: m.thumbnail, - title: m.title, - url: m.url, - views: 0, - source: 'apple_music', - requestedBy: context.requestedBy, - queryType: 'appleMusicSong', - metadata: { - source: info, - bridge: null - }, - requestMetadata: async () => { - return { - source: info, - bridge: (await this.options.bridgeProvider?.resolve(this, track))?.data - }; - } - }); - track.playlist = playlist; - track.extractor = this; - return track; - } - ); - - return { playlist, tracks: playlist.tracks }; - } - case QueryType.APPLE_MUSIC_PLAYLIST: { - const info = await AppleMusic.getPlaylistInfo(query); - if (!info) return this.createResponse(); - - const playlist = new Playlist(this.context.player, { - author: { - name: info.artist.name, - url: '' - }, - description: info.title, - id: info.id, - source: 'apple_music', - thumbnail: info.thumbnail, - title: info.title, - tracks: [], - type: 'playlist', - url: info.url, - rawPlaylist: info - }); - - playlist.tracks = info.tracks.map( - ( - m: any // eslint-disable-line - ) => { - const track: Track = new Track(this.context.player, { - author: m.artist.name, - description: m.title, - duration: typeof m.duration === 'number' ? Util.buildTimeCode(Util.parseMS(m.duration)) : m.duration, - thumbnail: m.thumbnail, - title: m.title, - url: m.url, - views: 0, - source: 'apple_music', - requestedBy: context.requestedBy, - queryType: 'appleMusicSong', - metadata: { - source: m, - bridge: null - }, - requestMetadata: async () => { - return { - source: m, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, track)).data : await pullYTMetadata(this, track) - }; - } - }); - - track.playlist = playlist; - track.extractor = this; - - return track; - } - ); - - return { playlist, tracks: playlist.tracks }; - } - case QueryType.APPLE_MUSIC_SONG: { - const info = await AppleMusic.getSongInfo(query); - if (!info) return this.createResponse(); - - const track: Track = new Track(this.context.player, { - author: info.artist.name, - description: info.title, - duration: typeof info.duration === 'number' ? Util.buildTimeCode(Util.parseMS(info.duration)) : info.duration, - thumbnail: info.thumbnail, - title: info.title, - url: info.url, - views: 0, - source: 'apple_music', - requestedBy: context.requestedBy, - queryType: context.type, - metadata: { - source: info, - bridge: null - }, - requestMetadata: async () => { - return { - source: info, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, track)).data : await pullYTMetadata(this, track) - }; - }, - }); - - track.extractor = this; - - return { playlist: null, tracks: [track] }; - } - default: - return { playlist: null, tracks: [] }; - } - } - - public async stream(info: Track): Promise { - if (this._stream) { - const stream = await this._stream(info.url, this); - if (typeof stream === 'string') return stream; - return stream; - } - - const provider = this.bridgeProvider; - if (!provider) throw new Error(`Could not find bridge provider for '${this.constructor.name}'`); - - const data = await provider.resolve(this, info); - if (!data) throw new Error('Failed to bridge this track'); - - info.setMetadata({ - ...(info.metadata || {}), - bridge: data.data - }); - - return await provider.stream(data); - } -} diff --git a/packages/extractor/src/extractors/AttachmentExtractor.ts b/packages/extractor/src/extractors/AttachmentExtractor.ts deleted file mode 100644 index e8c0bd6b6b..0000000000 --- a/packages/extractor/src/extractors/AttachmentExtractor.ts +++ /dev/null @@ -1,211 +0,0 @@ -// prettier-ignore -import { - BaseExtractor, - ExtractorInfo, - ExtractorSearchContext, - QueryType, - SearchQueryType, - Track, - Util -} from 'discord-player'; -import type { IncomingMessage } from 'http'; -import { createReadStream, existsSync } from 'fs'; -import { downloadStream } from '../internal/downloader'; -import * as fileType from 'file-type'; -import path from 'path'; -import { stat } from 'fs/promises'; - -const ATTACHMENT_HEADER = ['audio/', 'video/', 'application/ogg'] as const; - -export class AttachmentExtractor extends BaseExtractor { - public static identifier = 'com.discord-player.attachmentextractor' as const; - - // use lowest priority to avoid conflict with other extractors - public priority = 0; - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - if (typeof query !== 'string') return false; - return ([QueryType.ARBITRARY, QueryType.FILE] as SearchQueryType[]).some((r) => r === type); - } - - public async getRelatedTracks(track: Track) { - void track; - return this.createResponse(); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - switch (context.type) { - case QueryType.ARBITRARY: { - const data = (await downloadStream(query, context.requestOptions)) as IncomingMessage; - if (!ATTACHMENT_HEADER.some((r) => !!data.headers['content-type']?.startsWith(r))) return this.emptyResponse(); - - const trackInfo = { - title: ( - query - .split('/') - .filter((x) => x.length) - .pop() ?? 'Attachment' - ) - .split('?')[0] - .trim(), - duration: 0, - thumbnail: 'https://upload.wikimedia.org/wikipedia/commons/2/2a/ITunes_12.2_logo.png', - engine: query, - // eslint-disable-next-line - author: ((data as any).client?.servername as string) || 'Attachment', - // eslint-disable-next-line - description: ((data as any).client?.servername as string) || 'Attachment', - url: data.url || query - }; - - try { - // eslint-disable-next-line - const mediaplex = require('mediaplex') as typeof import('mediaplex'); - const timeout = this.context.player.options.probeTimeout ?? 5000; - - const { result, stream } = (await Promise.race([ - mediaplex.probeStream(data), - new Promise((_, r) => { - setTimeout(() => r(new Error('Timeout')), timeout); - }) - ])) as Awaited>; - - if (result) { - trackInfo.duration = result.duration * 1000; - - const metadata = mediaplex.readMetadata(result); - if (metadata.author) trackInfo.author = metadata.author; - if (metadata.title) trackInfo.title = metadata.title; - - trackInfo.description = `${trackInfo.title} by ${trackInfo.author}`; - } - - stream.destroy(); - } catch { - // - } - - const track = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), - description: trackInfo.description, - thumbnail: trackInfo.thumbnail, - views: 0, - author: trackInfo.author, - requestedBy: context.requestedBy, - source: 'arbitrary', - engine: trackInfo.url, - queryType: context.type, - metadata: trackInfo, - async requestMetadata() { - return trackInfo; - } - }); - - track.extractor = this; - - // @ts-expect-error - track.raw.isFile = false; - - return { playlist: null, tracks: [track] }; - } - case QueryType.FILE: { - if (!existsSync(query)) return this.emptyResponse(); - const fstat = await stat(query); - if (!fstat.isFile()) return this.emptyResponse(); - const mime = await fileType.fromFile(query).catch(() => null); - if (!mime || !ATTACHMENT_HEADER.some((r) => !!mime.mime.startsWith(r))) return this.emptyResponse(); - - const trackInfo = { - title: path.basename(query) || 'Attachment', - duration: 0, - thumbnail: 'https://upload.wikimedia.org/wikipedia/commons/2/2a/ITunes_12.2_logo.png', - engine: query, - author: 'Attachment', - description: 'Attachment', - url: query - }; - - try { - // eslint-disable-next-line - const mediaplex = require('mediaplex') as typeof import('mediaplex'); - - const timeout = this.context.player.options.probeTimeout ?? 5000; - - const { result, stream } = (await Promise.race([ - mediaplex.probeStream( - createReadStream(query, { - start: 0, - end: 1024 * 1024 * 10 - }) - ), - new Promise((_, r) => { - setTimeout(() => r(new Error('Timeout')), timeout); - }) - ])) as Awaited>; - - if (result) { - trackInfo.duration = result.duration * 1000; - - const metadata = mediaplex.readMetadata(result); - if (metadata.author) trackInfo.author = metadata.author; - if (metadata.title) trackInfo.title = metadata.title; - - trackInfo.description = `${trackInfo.title} by ${trackInfo.author}`; - } - - stream.destroy(); - } catch { - // - } - - const track = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), - description: trackInfo.description, - thumbnail: trackInfo.thumbnail, - views: 0, - author: trackInfo.author, - requestedBy: context.requestedBy, - source: 'arbitrary', - engine: trackInfo.url, - queryType: context.type, - metadata: trackInfo, - async requestMetadata() { - return trackInfo; - } - }); - - track.extractor = this; - - // @ts-expect-error - track.raw.isFile = true; - - return { playlist: null, tracks: [track] }; - } - default: - return this.emptyResponse(); - } - } - - public emptyResponse(): ExtractorInfo { - return { playlist: null, tracks: [] }; - } - - public async stream(info: Track) { - const engine = info.raw.engine as string; - // @ts-expect-error - const isFile = info.raw.isFile as boolean; - - if (!engine) throw new Error('Could not find stream source'); - - if (!isFile) { - return engine; - // return await downloadStream(engine); - } - - return createReadStream(engine); - } -} diff --git a/packages/extractor/src/extractors/BridgedExtractor.ts b/packages/extractor/src/extractors/BridgedExtractor.ts deleted file mode 100644 index c0650f26e2..0000000000 --- a/packages/extractor/src/extractors/BridgedExtractor.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { BaseExtractor, type ExtractorExecutionContext } from 'discord-player'; -import { BridgeProvider, BridgeSource, defaultBridgeProvider, IBridgeSource } from './common/BridgeProvider'; - -export interface BridgedOption { - bridgeProvider?: BridgeProvider; -} - -export class BridgedExtractor extends BaseExtractor { - public constructor(context: ExtractorExecutionContext, options?: T | undefined) { - super(context, options); - } - - public setBridgeProvider(provider: BridgeProvider) { - this.options.bridgeProvider = provider; - } - - public setBridgeProviderSource(source: BridgeSource | IBridgeSource) { - this.bridgeProvider.setBridgeSource(source); - } - - public get bridgeProvider() { - return this.options.bridgeProvider ?? defaultBridgeProvider; - } -} diff --git a/packages/extractor/src/extractors/LyricsExtractor.ts b/packages/extractor/src/extractors/LyricsExtractor.ts deleted file mode 100644 index 9e0740054a..0000000000 --- a/packages/extractor/src/extractors/LyricsExtractor.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Client as GeniusClient } from 'genius-lyrics'; - -// lazy load client -let client: GeniusClient; - -export function lyricsExtractor(apiKey?: string, force?: boolean) { - if (!client && !force) client = new GeniusClient(apiKey); - return { search, client }; -} - -function search(query: string) { - return new Promise((resolve, reject) => { - if (typeof query !== 'string') return reject(new TypeError(`Expected search query to be a string, received "${typeof query}"!`)); - - client.songs - .search(query) - .then(async (songs) => { - const data = { - title: songs[0].title, - fullTitle: songs[0].fullTitle, - id: songs[0].id, - thumbnail: songs[0].thumbnail, - image: songs[0].image, - url: songs[0].url, - artist: { - name: songs[0].artist.name, - id: songs[0].artist.id, - url: songs[0].artist.url, - image: songs[0].artist.image - }, - lyrics: await songs[0].lyrics(false) - }; - - resolve(data); - }) - .catch(() => { - reject(new Error('Could not parse lyrics')); - }); - }); -} - -export interface LyricsData { - title: string; - fullTitle: string; - id: number; - thumbnail: string; - image: string; - url: string; - artist: { - name: string; - id: number; - url: string; - image: string; - }; - lyrics: string; -} diff --git a/packages/extractor/src/extractors/ReverbnationExtractor.ts b/packages/extractor/src/extractors/ReverbnationExtractor.ts deleted file mode 100644 index 093c9a4681..0000000000 --- a/packages/extractor/src/extractors/ReverbnationExtractor.ts +++ /dev/null @@ -1,79 +0,0 @@ -// prettier-ignore -import { - BaseExtractor, - ExtractorInfo, - ExtractorSearchContext, - QueryType, - SearchQueryType, - Track, - Util -} from 'discord-player'; -import reverbnation from 'reverbnation-scraper'; - -export class ReverbnationExtractor extends BaseExtractor { - public static identifier = 'com.discord-player.reverbnationextractor' as const; - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - if (typeof query !== 'string') return false; - return ([QueryType.REVERBNATION] as SearchQueryType[]).some((r) => r === type); - } - - public async getRelatedTracks(track: Track) { - void track; - return this.createResponse(); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - switch (context.type) { - case QueryType.REVERBNATION: { - const trackInfo = await reverbnation.getInfo(query).catch(Util.noop); - - if (!trackInfo) return this.emptyResponse(); - - const track = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), - description: trackInfo.lyrics || `${trackInfo.title} by ${trackInfo.artist.name}`, - thumbnail: trackInfo.thumbnail, - views: 0, - author: trackInfo.artist.name, - requestedBy: context.requestedBy, - source: 'arbitrary', - engine: trackInfo.streamURL, - queryType: context.type, - metadata: trackInfo, - async requestMetadata() { - return trackInfo; - } - }); - - track.extractor = this; - - return { playlist: null, tracks: [track] }; - } - default: - return this.emptyResponse(); - } - } - - public emptyResponse(): ExtractorInfo { - return { playlist: null, tracks: [] }; - } - - public async stream(info: Track) { - const engine = info.raw.engine as string; - if (engine) { - return engine; - } - - const track = await reverbnation.getInfo(info.url).catch(Util.noop); - if (!track || !track.streamURL) throw new Error('Could not extract stream from this source'); - - info.raw.engine = { - streamURL: track.streamURL - }; - - return track.streamURL; - } -} diff --git a/packages/extractor/src/extractors/SoundCloudExtractor.ts b/packages/extractor/src/extractors/SoundCloudExtractor.ts deleted file mode 100644 index 98b76b550f..0000000000 --- a/packages/extractor/src/extractors/SoundCloudExtractor.ts +++ /dev/null @@ -1,225 +0,0 @@ -// prettier-ignore -import { - BaseExtractor, - ExtractorInfo, - ExtractorSearchContext, - type GuildQueueHistory, - Playlist, - QueryType, - SearchQueryType, - Track, - Util -} from 'discord-player'; -import * as SoundCloud from 'soundcloud.ts'; -import { filterSoundCloudPreviews } from './common/helper'; - -export interface SoundCloudExtractorInit { - clientId?: string; - oauthToken?: string; - proxy?: string; -} - -export class SoundCloudExtractor extends BaseExtractor { - public static identifier = 'com.discord-player.soundcloudextractor' as const; - public static instance: SoundCloudExtractor | null = null; - - public internal = new SoundCloud.default({ - clientId: this.options.clientId, - oauthToken: this.options.oauthToken, - proxy: this.options.proxy - }); - - public async activate(): Promise { - this.protocols = ['scsearch', 'soundcloud']; - SoundCloudExtractor.instance = this; - } - - public async deactivate(): Promise { - this.protocols = []; - SoundCloudExtractor.instance = null; - } - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - if (typeof query !== 'string') return false; - // prettier-ignore - return ([ - QueryType.SOUNDCLOUD, - QueryType.SOUNDCLOUD_PLAYLIST, - QueryType.SOUNDCLOUD_SEARCH, - QueryType.SOUNDCLOUD_TRACK, - QueryType.AUTO, - QueryType.AUTO_SEARCH - ] as SearchQueryType[]).some((r) => r === type); - } - - public async getRelatedTracks(track: Track, history: GuildQueueHistory) { - if (track.queryType === QueryType.SOUNDCLOUD_TRACK) { - const data = await this.internal.tracks.relatedV2(track.url, 5); - - const unique = filterSoundCloudPreviews(data).filter((t) => !history.tracks.some((h) => h.url === t.permalink_url)); - - return this.createResponse( - null, - (unique.length > 0 ? unique : data).map((trackInfo) => { - const newTrack = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.permalink_url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), - description: trackInfo.description ?? '', - thumbnail: trackInfo.artwork_url, - views: trackInfo.playback_count, - author: trackInfo.user.username, - requestedBy: track.requestedBy, - source: 'soundcloud', - engine: trackInfo, - queryType: QueryType.SOUNDCLOUD_TRACK, - metadata: trackInfo, - requestMetadata: async () => { - return trackInfo; - }, - cleanTitle: trackInfo.title - }); - - newTrack.extractor = this; - - return newTrack; - }) - ); - } - - return this.createResponse(); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - if (context.protocol === 'scsearch') context.type = QueryType.SOUNDCLOUD_SEARCH; - switch (context.type) { - case QueryType.SOUNDCLOUD_TRACK: { - const trackInfo = await this.internal.tracks.getV2(query).catch(Util.noop); - - if (!trackInfo) return this.emptyResponse(); - - const track = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.permalink_url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), - description: trackInfo.description ?? '', - thumbnail: trackInfo.artwork_url, - views: trackInfo.playback_count, - author: trackInfo.user.username, - requestedBy: context.requestedBy, - source: 'soundcloud', - engine: trackInfo, - queryType: context.type, - metadata: trackInfo, - requestMetadata: async () => { - return trackInfo; - }, - cleanTitle: trackInfo.title - }); - - track.extractor = this; - - return { playlist: null, tracks: [track] }; - } - case QueryType.SOUNDCLOUD_PLAYLIST: { - const data = await this.internal.playlists.getV2(query).catch(Util.noop); - if (!data) return { playlist: null, tracks: [] }; - - const res = new Playlist(this.context.player, { - title: data.title, - description: data.description ?? '', - thumbnail: data.artwork_url ?? data.tracks[0].artwork_url, - type: 'playlist', - source: 'soundcloud', - author: { - name: data.user.username, - url: data.user.permalink_url - }, - tracks: [], - id: `${data.id}`, - url: data.permalink_url, - rawPlaylist: data - }); - - for (const song of data.tracks) { - const track = new Track(this.context.player, { - title: song.title, - description: song.description ?? '', - author: song.user.username, - url: song.permalink_url, - thumbnail: song.artwork_url, - duration: Util.buildTimeCode(Util.parseMS(song.duration)), - views: song.playback_count, - requestedBy: context.requestedBy, - playlist: res, - source: 'soundcloud', - engine: song, - queryType: context.type, - metadata: song, - requestMetadata: async () => { - return song; - }, - cleanTitle: song.title - }); - track.extractor = this; - track.playlist = res; - res.tracks.push(track); - } - - return { playlist: res, tracks: res.tracks }; - } - default: { - let tracks = await this.internal.tracks - .searchV2({ q: query }) - .then((t) => t.collection) - .catch(Util.noop); - - if (!tracks) tracks = await this.internal.tracks.searchAlt(query).catch(Util.noop); - - if (!tracks || !tracks.length) return this.emptyResponse(); - - tracks = filterSoundCloudPreviews(tracks); - - const resolvedTracks: Track[] = []; - - for (const trackInfo of tracks) { - if (!trackInfo.streamable) continue; - const track = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.permalink_url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration)), - description: trackInfo.description ?? '', - thumbnail: trackInfo.artwork_url, - views: trackInfo.playback_count, - author: trackInfo.user.username, - requestedBy: context.requestedBy, - source: 'soundcloud', - engine: trackInfo, - queryType: 'soundcloudTrack', - metadata: trackInfo, - requestMetadata: async () => { - return trackInfo; - } - }); - - track.extractor = this; - - resolvedTracks.push(track); - } - - return { playlist: null, tracks: resolvedTracks }; - } - } - } - - public emptyResponse(): ExtractorInfo { - return { playlist: null, tracks: [] }; - } - - public async stream(info: Track) { - const url = await this.internal.util.streamLink(info.url).catch(Util.noop); - if (!url) throw new Error('Could not extract stream from this track source'); - - return url; - } -} diff --git a/packages/extractor/src/extractors/SpotifyExtractor.ts b/packages/extractor/src/extractors/SpotifyExtractor.ts deleted file mode 100644 index 5042137886..0000000000 --- a/packages/extractor/src/extractors/SpotifyExtractor.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { ExtractorInfo, ExtractorSearchContext, ExtractorStreamable, Playlist, QueryType, SearchQueryType, Track, Util } from 'discord-player'; -import type { Readable } from 'stream'; -import { StreamFN, fetch, pullYTMetadata } from './common/helper'; -import spotify, { Spotify, SpotifyAlbum, SpotifyPlaylist, SpotifySong } from 'spotify-url-info'; -import { SpotifyAPI } from '../internal'; -import { BridgeProvider } from './common/BridgeProvider'; -import { BridgedExtractor } from './BridgedExtractor'; - -const re = /^(?:https:\/\/open\.spotify\.com\/(intl-([a-z]|[A-Z]){0,3}\/)?(?:user\/[A-Za-z0-9]+\/)?|spotify:)(album|playlist|track)(?:[/:])([A-Za-z0-9]+).*$/; - -export interface SpotifyExtractorInit { - clientId?: string | null; - clientSecret?: string | null; - createStream?: (ext: SpotifyExtractor, url: string) => Promise; - bridgeProvider?: BridgeProvider; -} - -export class SpotifyExtractor extends BridgedExtractor { - public static identifier = 'com.discord-player.spotifyextractor' as const; - private _stream!: StreamFN; - private _lib!: Spotify; - private _credentials = { - clientId: this.options.clientId || process.env.DP_SPOTIFY_CLIENT_ID || null, - clientSecret: this.options.clientSecret || process.env.DP_SPOTIFY_CLIENT_SECRET || null - }; - public internal = new SpotifyAPI(this._credentials); - - public async activate(): Promise { - this.protocols = ['spsearch', 'spotify']; - this._lib = spotify(fetch); - if (this.internal.isTokenExpired()) await this.internal.requestToken(); - - const fn = this.options.createStream; - if (typeof fn === 'function') { - this._stream = (q: string) => { - return fn(this, q); - }; - } - } - - public async deactivate() { - this._stream = undefined as unknown as StreamFN; - this._lib = undefined as unknown as Spotify; - this.protocols = []; - } - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - // prettier-ignore - return ([ - QueryType.SPOTIFY_ALBUM, - QueryType.SPOTIFY_PLAYLIST, - QueryType.SPOTIFY_SONG, - QueryType.SPOTIFY_SEARCH, - QueryType.AUTO, - QueryType.AUTO_SEARCH - ]).some((t) => t === type); - } - - public async getRelatedTracks(track: Track) { - return await this.handle(track.author || track.title, { - type: QueryType.SPOTIFY_SEARCH, - requestedBy: track.requestedBy - }); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - if (context.protocol === 'spsearch') context.type = QueryType.SPOTIFY_SEARCH; - switch (context.type) { - case QueryType.AUTO: - case QueryType.AUTO_SEARCH: - case QueryType.SPOTIFY_SEARCH: { - const data = await this.internal.search(query); - if (!data) return this.createResponse(); - - return this.createResponse( - null, - data.map((spotifyData) => { - const track: Track = new Track(this.context.player, { - title: spotifyData.title, - description: `${spotifyData.title} by ${spotifyData.artist}`, - author: spotifyData.artist ?? 'Unknown Artist', - url: spotifyData.url, - thumbnail: spotifyData.thumbnail || 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration ?? 0)), - views: 0, - requestedBy: context.requestedBy, - source: 'spotify', - queryType: QueryType.SPOTIFY_SONG, - metadata: { - source: spotifyData, - bridge: null - }, - requestMetadata: async () => { - return { - source: spotifyData, - bridge: (await this.options.bridgeProvider?.resolve(this, track))?.data - }; - } - }); - - track.extractor = this; - - return track; - }) - ); - } - case QueryType.SPOTIFY_SONG: { - const spotifyData: SpotifySong | void = await this._lib.getData(query, context.requestOptions as unknown as RequestInit).catch(Util.noop); - if (!spotifyData) return { playlist: null, tracks: [] }; - const spotifyTrack: Track = new Track(this.context.player, { - title: spotifyData.title, - description: `${spotifyData.name} by ${spotifyData.artists.map((m) => m.name).join(', ')}`, - author: spotifyData.artists[0]?.name ?? 'Unknown Artist', - url: spotifyData.id ? `https://open.spotify.com/track/${spotifyData.id}` : query, - thumbnail: spotifyData.coverArt?.sources?.[0]?.url || 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration ?? spotifyData.maxDuration ?? 0)), - views: 0, - requestedBy: context.requestedBy, - source: 'spotify', - queryType: context.type, - metadata: { - source: spotifyData, - bridge: null - }, - requestMetadata: async () => { - return { - source: spotifyData, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, spotifyTrack)).data : await pullYTMetadata(this, spotifyTrack) - }; - } - }); - - spotifyTrack.extractor = this; - - return { playlist: null, tracks: [spotifyTrack] }; - } - case QueryType.SPOTIFY_PLAYLIST: { - try { - const { queryType, id } = this.parse(query); - if (queryType !== 'playlist') throw 'err'; - - const spotifyPlaylist = await this.internal.getPlaylist(id); - if (!spotifyPlaylist) throw 'err'; - - const playlist = new Playlist(this.context.player, { - title: spotifyPlaylist.name, - description: spotifyPlaylist.name ?? '', - thumbnail: spotifyPlaylist.thumbnail ?? 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - type: 'playlist', - source: 'spotify', - author: { - name: spotifyPlaylist.author ?? 'Unknown Artist', - url: null as unknown as string - }, - tracks: [], - id: spotifyPlaylist.id, - url: spotifyPlaylist.url || query, - rawPlaylist: spotifyPlaylist - }); - - playlist.tracks = spotifyPlaylist.tracks.map((spotifyData) => { - const data: Track = new Track(this.context.player, { - title: spotifyData.title, - description: `${spotifyData.title} by ${spotifyData.artist}`, - author: spotifyData.artist ?? 'Unknown Artist', - url: spotifyData.url, - thumbnail: spotifyData.thumbnail || 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration ?? 0)), - views: 0, - requestedBy: context.requestedBy, - source: 'spotify', - queryType: QueryType.SPOTIFY_SONG, - metadata: { - source: spotifyData, - bridge: null - }, - requestMetadata: async () => { - return { - source: spotifyData, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, data)).data : await pullYTMetadata(this, data) - }; - } - }); - data.extractor = this; - data.playlist = playlist; - return data; - }) as Track[]; - - return { playlist, tracks: playlist.tracks }; - } catch { - const spotifyPlaylist: SpotifyPlaylist | void = await this._lib.getData(query, context.requestOptions as unknown as RequestInit).catch(Util.noop); - if (!spotifyPlaylist) return { playlist: null, tracks: [] }; - - const playlist = new Playlist(this.context.player, { - title: spotifyPlaylist.name ?? spotifyPlaylist.title, - description: spotifyPlaylist.title ?? '', - thumbnail: spotifyPlaylist.coverArt?.sources?.[0]?.url ?? 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - type: spotifyPlaylist.type, - source: 'spotify', - author: { - name: spotifyPlaylist.subtitle ?? 'Unknown Artist', - url: null as unknown as string - }, - tracks: [], - id: spotifyPlaylist.id, - url: spotifyPlaylist.id ? `https://open.spotify.com/playlist/${spotifyPlaylist.id}` : query, - rawPlaylist: spotifyPlaylist - }); - - playlist.tracks = spotifyPlaylist.trackList.map((m) => { - const data: Track = new Track(this.context.player, { - title: m.title ?? '', - description: m.title ?? '', - author: m.subtitle ?? 'Unknown Artist', - url: m.uid ? `https://open.spotify.com/tracks/${m.uid}` : query, - thumbnail: 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - duration: Util.buildTimeCode(Util.parseMS(m.duration)), - views: 0, - requestedBy: context.requestedBy, - playlist, - source: 'spotify', - queryType: 'spotifySong', - metadata: { - source: m, - bridge: null - }, - requestMetadata: async () => { - return { - source: m, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, data)).data : await pullYTMetadata(this, data) - }; - } - }); - data.extractor = this; - data.playlist = playlist; - return data; - }) as Track[]; - - return { playlist, tracks: playlist.tracks }; - } - } - case QueryType.SPOTIFY_ALBUM: { - try { - const { queryType, id } = this.parse(query); - if (queryType !== 'album') throw 'err'; - - const spotifyAlbum = await this.internal.getAlbum(id); - if (!spotifyAlbum) throw 'err'; - - const playlist = new Playlist(this.context.player, { - title: spotifyAlbum.name, - description: spotifyAlbum.name ?? '', - thumbnail: spotifyAlbum.thumbnail ?? 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - type: 'album', - source: 'spotify', - author: { - name: spotifyAlbum.author ?? 'Unknown Artist', - url: null as unknown as string - }, - tracks: [], - id: spotifyAlbum.id, - url: spotifyAlbum.url || query, - rawPlaylist: spotifyAlbum - }); - - playlist.tracks = spotifyAlbum.tracks.map((spotifyData) => { - const data: Track = new Track(this.context.player, { - title: spotifyData.title, - description: `${spotifyData.title} by ${spotifyData.artist}`, - author: spotifyData.artist ?? 'Unknown Artist', - url: spotifyData.url, - thumbnail: spotifyData.thumbnail || 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - duration: Util.buildTimeCode(Util.parseMS(spotifyData.duration ?? 0)), - views: 0, - requestedBy: context.requestedBy, - source: 'spotify', - queryType: QueryType.SPOTIFY_SONG, - metadata: { - source: spotifyData, - bridge: null - }, - requestMetadata: async () => { - return { - source: spotifyData, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, data)).data : await pullYTMetadata(this, data) - }; - } - }); - data.extractor = this; - data.playlist = playlist; - return data; - }) as Track[]; - - return { playlist, tracks: playlist.tracks }; - } catch { - const album: SpotifyAlbum | void = await this._lib.getData(query, context.requestOptions as unknown as RequestInit).catch(Util.noop); - if (!album) return { playlist: null, tracks: [] }; - - const playlist = new Playlist(this.context.player, { - title: album.name ?? album.title, - description: album.title ?? '', - thumbnail: album.coverArt?.sources?.[0]?.url ?? 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - type: album.type, - source: 'spotify', - author: { - name: album.subtitle ?? 'Unknown Artist', - url: null as unknown as string - }, - tracks: [], - id: album.id, - url: album.id ? `https://open.spotify.com/playlist/${album.id}` : query, - rawPlaylist: album - }); - - playlist.tracks = album.trackList.map((m) => { - const data: Track = new Track(this.context.player, { - title: m.title ?? '', - description: m.title ?? '', - author: m.subtitle ?? 'Unknown Artist', - url: m.uid ? `https://open.spotify.com/tracks/${m.uid}` : query, - thumbnail: 'https://www.scdn.co/i/_global/twitter_card-default.jpg', - duration: Util.buildTimeCode(Util.parseMS(m.duration)), - views: 0, - requestedBy: context.requestedBy, - playlist, - source: 'spotify', - queryType: 'spotifySong', - metadata: { - source: m, - bridge: null - }, - requestMetadata: async () => { - return { - source: m, - bridge: this.options.bridgeProvider ? (await this.options.bridgeProvider.resolve(this, data)).data : await pullYTMetadata(this, data) - }; - } - }); - data.extractor = this; - data.playlist = playlist; - return data; - }) as Track[]; - - return { playlist, tracks: playlist.tracks }; - } - } - default: - return { playlist: null, tracks: [] }; - } - } - - public async stream(info: Track): Promise { - if (this._stream) { - const stream = await this._stream(info.url, this); - if (typeof stream === 'string') return stream; - return stream; - } - - const provider = this.bridgeProvider; - if (!provider) throw new Error(`Could not find bridge provider for '${this.constructor.name}'`); - - const data = await provider.resolve(this, info); - if (!data) throw new Error('Failed to bridge this track'); - - info.setMetadata({ - ...(info.metadata || {}), - bridge: data.data - }); - - return await provider.stream(data); - } - - public parse(q: string) { - const [, , , queryType, id] = re.exec(q) || []; - - return { queryType, id }; - } -} diff --git a/packages/extractor/src/extractors/VimeoExtractor.ts b/packages/extractor/src/extractors/VimeoExtractor.ts deleted file mode 100644 index 9cffde0ffc..0000000000 --- a/packages/extractor/src/extractors/VimeoExtractor.ts +++ /dev/null @@ -1,84 +0,0 @@ -// prettier-ignore -import { - BaseExtractor, - ExtractorInfo, - ExtractorSearchContext, - QueryType, - SearchQueryType, - Track, - Util -} from 'discord-player'; -import { Vimeo } from '../internal/Vimeo'; - -export class VimeoExtractor extends BaseExtractor { - public static identifier = 'com.discord-player.vimeoextractor' as const; - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - if (typeof query !== 'string') return false; - return ([QueryType.VIMEO] as SearchQueryType[]).some((r) => r === type); - } - - public async getRelatedTracks(track: Track) { - void track; - return this.createResponse(); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - switch (context.type) { - case QueryType.VIMEO: { - const trackInfo = await Vimeo.getInfo( - query - .split('/') - .filter((x) => !!x) - .pop()! - ).catch(Util.noop); - - if (!trackInfo) return this.emptyResponse(); - - const track = new Track(this.context.player, { - title: trackInfo.title, - url: trackInfo.url, - duration: Util.buildTimeCode(Util.parseMS(trackInfo.duration || 0)), - description: `${trackInfo.title} by ${trackInfo.author.name}`, - thumbnail: trackInfo.thumbnail, - views: 0, - author: trackInfo.author.name, - requestedBy: context.requestedBy, - source: 'arbitrary', - engine: trackInfo.stream, - queryType: context.type, - metadata: trackInfo, - async requestMetadata() { - return trackInfo; - } - }); - - track.extractor = this; - - return { playlist: null, tracks: [track] }; - } - default: - return this.emptyResponse(); - } - } - - public emptyResponse(): ExtractorInfo { - return { playlist: null, tracks: [] }; - } - - public async stream(info: Track) { - const engine = info.raw.engine as string; - if (engine) { - return engine; - } - - const track = await Vimeo.getInfo(info.url).catch(Util.noop); - if (!track || !track.stream) throw new Error('Could not extract stream from this source'); - - info.raw.engine = { - streamURL: track.stream - }; - - return track.stream; - } -} diff --git a/packages/extractor/src/extractors/YoutubeExtractor.ts b/packages/extractor/src/extractors/YoutubeExtractor.ts deleted file mode 100644 index 17d8a853a4..0000000000 --- a/packages/extractor/src/extractors/YoutubeExtractor.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { Video, YouTube } from 'youtube-sr'; - -// prettier-ignore -import { - BaseExtractor, - ExtractorInfo, - ExtractorSearchContext, - type GuildQueueHistory, - Playlist, - QueryType, - SearchQueryType, - Track, - Util, - ExtractorStreamable -} from 'discord-player'; - -import { StreamFN, YouTubeLibs, loadYtdl, makeYTSearch } from './common/helper'; -import type { Readable } from 'stream'; - -// taken from ytdl-core -const validQueryDomains = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'gaming.youtube.com']); -const validPathDomains = /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|v|shorts)\/)/; -const idRegex = /^[a-zA-Z0-9-_]{11}$/; - -export interface YoutubeExtractorInit { - createStream?: (ext: YoutubeExtractor, url: string) => Promise; -} - -export class YoutubeExtractor extends BaseExtractor { - public static identifier = 'com.discord-player.youtubeextractor' as const; - public _stream!: StreamFN; - public _ytLibName!: string; - public static instance: YoutubeExtractor | null; - - public async activate() { - this.protocols = ['ytsearch', 'youtube']; - const fn = this.options.createStream; - - if (typeof fn === 'function') { - this._stream = (q: string) => { - return fn(this, q); - }; - } else { - const { stream, name } = await loadYtdl(this.context.player.options.ytdlOptions); - this._stream = stream; - this._ytLibName = name; - } - - YoutubeExtractor.instance = this; - } - - public async deactivate(): Promise { - this.protocols = []; - YoutubeExtractor.instance = null; - } - - public async validate(query: string, type?: SearchQueryType | null | undefined): Promise { - if (typeof query !== 'string') return false; - // prettier-ignore - return ([ - QueryType.YOUTUBE, - QueryType.YOUTUBE_PLAYLIST, - QueryType.YOUTUBE_SEARCH, - QueryType.YOUTUBE_VIDEO, - QueryType.AUTO, - QueryType.AUTO_SEARCH - ] as SearchQueryType[]).some((r) => r === type); - } - - public async handle(query: string, context: ExtractorSearchContext): Promise { - if (context.protocol === 'ytsearch') context.type = QueryType.YOUTUBE_SEARCH; - query = query.includes('youtube.com') ? query.replace(/(m(usic)?|gaming)\./, '') : query; - if (!query.includes('list=RD') && YoutubeExtractor.validateURL(query)) context.type = QueryType.YOUTUBE_VIDEO; - - switch (context.type) { - case QueryType.YOUTUBE_PLAYLIST: { - const ytpl = await YouTube.getPlaylist(query, { - fetchAll: true, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - limit: (context.requestOptions as any)?.limit, - requestOptions: context.requestOptions as unknown as RequestInit - }).catch(Util.noop); - if (!ytpl) return this.emptyResponse(); - - const playlist = new Playlist(this.context.player, { - title: ytpl.title!, - thumbnail: ytpl.thumbnail?.displayThumbnailURL('maxresdefault') as string, - description: ytpl.title || '', - type: 'playlist', - source: 'youtube', - author: { - name: ytpl.channel!.name as string, - url: ytpl.channel!.url as string - }, - tracks: [], - id: ytpl.id as string, - url: ytpl.url as string, - rawPlaylist: ytpl - }); - - playlist.tracks = ytpl.videos.map((video) => { - const track = new Track(this.context.player, { - title: video.title as string, - description: video.description as string, - author: video.channel?.name as string, - url: video.url, - requestedBy: context.requestedBy, - thumbnail: video.thumbnail!.url as string, - views: video.views, - duration: video.durationFormatted, - raw: video, - playlist: playlist, - source: 'youtube', - queryType: 'youtubeVideo', - metadata: video, - async requestMetadata() { - return video; - } - }); - - track.extractor = this; - track.playlist = playlist; - return track; - }); - - return { playlist, tracks: playlist.tracks }; - } - case QueryType.YOUTUBE_VIDEO: { - const id = /[a-zA-Z0-9-_]{11}/.exec(query); - if (!id?.[0]) return this.emptyResponse(); - const video = await YouTube.getVideo(`https://www.youtube.com/watch?v=${id}`, context.requestOptions as unknown as RequestInit).catch(Util.noop); - if (!video) return this.emptyResponse(); - - // @ts-expect-error - video.source = 'youtube'; - - const track = new Track(this.context.player, { - title: video.title!, - description: video.description!, - author: video.channel?.name as string, - url: video.url, - requestedBy: context.requestedBy, - thumbnail: video.thumbnail?.displayThumbnailURL('maxresdefault') as string, - views: video.views, - duration: video.durationFormatted, - source: 'youtube', - raw: video, - queryType: context.type, - metadata: video, - async requestMetadata() { - return video; - } - }); - - track.extractor = this; - - return { - playlist: null, - tracks: [track] - }; - } - default: { - const tracks = await this._makeYTSearch(query, context); - return { playlist: null, tracks }; - } - } - } - - private async _makeYTSearch(query: string, context: ExtractorSearchContext) { - const res = await makeYTSearch(query, context.requestOptions).catch(Util.noop); - if (!res || !res.length) return []; - - return res.map((video) => { - // @ts-expect-error - video.source = 'youtube'; - - const track = new Track(this.context.player, { - title: video.title!, - description: video.description!, - author: video.channel?.name as string, - url: video.url, - requestedBy: context.requestedBy, - thumbnail: video.thumbnail?.displayThumbnailURL('maxresdefault') as string, - views: video.views, - duration: video.durationFormatted, - source: 'youtube', - raw: video, - queryType: context.type!, - metadata: video, - async requestMetadata() { - return video; - } - }); - - track.extractor = this; - - return track; - }); - } - - public async getRelatedTracks(track: Track, history: GuildQueueHistory) { - let info: Video[] | void = undefined; - - if (YoutubeExtractor.validateURL(track.url)) - info = await YouTube.getVideo(track.url) - .then((x) => x.videos) - .catch(Util.noop); - - // fallback - if (!info) - info = await YouTube.search(track.author || track.title, { limit: 5, type: 'video' }) - .then((x) => x) - .catch(Util.noop); - - if (!info?.length) { - return this.createResponse(); - } - - const unique = info.filter((t) => !history.tracks.some((x) => x.url === t.url)); - - const similar = (unique.length > 0 ? unique : info).map((video) => { - const t = new Track(this.context.player, { - title: video.title!, - url: `https://www.youtube.com/watch?v=${video.id}`, - duration: video.durationFormatted || Util.buildTimeCode(Util.parseMS(video.duration * 1000)), - description: video.title!, - thumbnail: typeof video.thumbnail === 'string' ? video.thumbnail! : video.thumbnail!.url!, - views: video.views, - author: video.channel!.name!, - requestedBy: track.requestedBy, - source: 'youtube', - queryType: 'youtubeVideo', - metadata: video, - async requestMetadata() { - return video; - } - }); - - t.extractor = this; - - return t; - }); - - return this.createResponse(null, similar); - } - - public emptyResponse(): ExtractorInfo { - return { playlist: null, tracks: [] }; - } - - public async stream(info: Track): Promise { - if (!this._stream) { - throw new Error(`Could not find youtube streaming library. Install one of ${YouTubeLibs.join(', ')}`); - } - - let url = info.url; - url = url.includes('youtube.com') ? url.replace(/(m(usic)?|gaming)\./, '') : url; - - return this._stream(url, this, this.supportsDemux); - } - - public static validateURL(link: string) { - try { - YoutubeExtractor.parseURL(link); - return true; - } catch { - return false; - } - } - - public static validateId(id: string) { - return idRegex.test(id.trim()); - } - - public static parseURL(link: string) { - const parsed = new URL(link.trim()); - let id = parsed.searchParams.get('v'); - if (validPathDomains.test(link.trim()) && !id) { - const paths = parsed.pathname.split('/'); - id = parsed.host === 'youtu.be' ? paths[1] : paths[2]; - } else if (parsed.hostname && !validQueryDomains.has(parsed.hostname)) { - throw Error('Not a YouTube domain'); - } - if (!id) { - throw Error(`No video id found: "${link}"`); - } - id = id.substring(0, 11); - if (!this.validateId(id)) { - throw TypeError(`Video id (${id}) does not match expected ` + `format (${idRegex.toString()})`); - } - return id; - } -} - -export { YoutubeExtractor as YouTubeExtractor }; diff --git a/packages/extractor/src/extractors/common/BridgeProvider.ts b/packages/extractor/src/extractors/common/BridgeProvider.ts deleted file mode 100644 index fc1a94180a..0000000000 --- a/packages/extractor/src/extractors/common/BridgeProvider.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { BaseExtractor, Track } from 'discord-player'; -import { SoundcloudTrackV2 } from 'soundcloud.ts'; -import { Video } from 'youtube-sr'; -import { SoundCloudExtractor } from '../SoundCloudExtractor'; -import { YouTubeExtractor } from '../YoutubeExtractor'; -import { pullSCMetadata, pullYTMetadata } from './helper'; - -export enum BridgeSource { - /** - * Automatically resolve the bridge source - */ - Auto = 'auto', - /** - * Use SoundCloud as the bridge source - */ - SoundCloud = 'soundcloud', - /** - * Use YouTube as the bridge source - */ - YouTube = 'youtube' -} - -export type IBridgeSource = 'soundcloud' | 'youtube' | 'auto'; - -export class BridgeProvider { - public bridgeSource: BridgeSource = BridgeSource.SoundCloud; - - public constructor(source: IBridgeSource) { - this.setBridgeSource(source); - } - - public setBridgeSource(source: BridgeSource | IBridgeSource) { - switch (source) { - case 'soundcloud': - case BridgeSource.SoundCloud: - this.bridgeSource = BridgeSource.SoundCloud; - break; - case 'youtube': - case BridgeSource.YouTube: - this.bridgeSource = BridgeSource.YouTube; - break; - case 'auto': - case BridgeSource.Auto: - this.bridgeSource = BridgeSource.Auto; - break; - default: - throw new TypeError('invalid bridge source'); - } - } - - public isSoundCloud() { - return this.bridgeSource === BridgeSource.SoundCloud; - } - - public isYouTube() { - return this.bridgeSource === BridgeSource.YouTube; - } - - public isAuto() { - return this.bridgeSource === BridgeSource.Auto; - } - - public resolveProvider() { - if (this.isAuto()) { - if (YouTubeExtractor.instance && !isExtDisabled(YouTubeExtractor.instance)) { - return BridgeSource.YouTube; - } - - if (SoundCloudExtractor.instance && !isExtDisabled(SoundCloudExtractor.instance)) { - return BridgeSource.SoundCloud; - } - - throw new Error('Could not find any available extractors for automatic bridging.'); - } - - return this.bridgeSource; - } - - public async resolve(ext: BaseExtractor, track: Track) { - const isSoundcloud = this.resolveProvider() === BridgeSource.SoundCloud; - const bridgefn = isSoundcloud ? pullSCMetadata : pullYTMetadata; - - // patch query - const oldQc = ext.createBridgeQuery; - if (isSoundcloud) ext.createBridgeQuery = (track) => `${track.author} ${track.title}`; - const res = await bridgefn(ext, track); - - ext.debug(`Extracted bridge metadata using ${isSoundcloud ? 'soundcloud' : 'youtube'} extractor: ${JSON.stringify(res)}`); - - ext.createBridgeQuery = oldQc; - - return { source: isSoundcloud ? 'soundcloud' : 'youtube', data: res } as BridgedMetadata; - } - - public async stream(meta: BridgedMetadata) { - if (!meta.data) throw new Error('Could not find bridge metadata info.'); - - if (meta.source === 'soundcloud') { - if (!SoundCloudExtractor.instance) { - throw new Error('Could not find soundcloud extractor, make sure SoundCloudExtractor is instantiated properly.'); - } - - if (isExtDisabled(SoundCloudExtractor.instance)) { - throw new Error('Cannot stream, SoundCloudExtractor is disabled.'); - } - - return await SoundCloudExtractor.instance.internal.util.streamLink(meta.data as SoundcloudTrackV2, 'progressive'); - } else if (meta.source === 'youtube') { - if (!YouTubeExtractor.instance) { - throw new Error('Could not find youtube extractor, make sure YouTubeExtractor is instantiated properly.'); - } - - if (isExtDisabled(YouTubeExtractor.instance)) { - throw new Error('Cannot stream, YouTubeExtractor is disabled.'); - } - - return YouTubeExtractor.instance._stream((meta.data as Video).url, YouTubeExtractor.instance, YouTubeExtractor.instance.supportsDemux); - } else { - throw new TypeError('invalid bridge source'); - } - } -} - -function isExtDisabled(ext: BaseExtractor) { - const streamBlocked = !!ext.context.player.options.blockStreamFrom?.some((x) => x === ext.identifier); - // const extBlocked = !!ext.context.player.options.blockExtractors?.some((x) => x === ext.identifier); - - return streamBlocked; -} - -interface BridgedMetadata { - source: IBridgeSource; - data: SoundcloudTrackV2 | Video | null; -} - -export const defaultBridgeProvider = new BridgeProvider(BridgeSource.Auto); -export const createBridgeProvider = (source: BridgeSource) => new BridgeProvider(source); diff --git a/packages/extractor/src/extractors/common/helper.ts b/packages/extractor/src/extractors/common/helper.ts deleted file mode 100644 index b51078b108..0000000000 --- a/packages/extractor/src/extractors/common/helper.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { BaseExtractor, Track } from 'discord-player'; -import { YouTube } from 'youtube-sr'; -import { SoundCloudExtractor } from '../SoundCloudExtractor'; -import unfetch from 'isomorphic-unfetch'; -import http from 'http'; -import https from 'https'; -import type * as SoundCloud from 'soundcloud.ts'; - -let factory: { - name: string; - stream: StreamFN; - lib: string; -}; - -export const createImport = (lib: string) => import(lib).catch(() => null); -export const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.49'; -export const fetch = unfetch; - -export const YouTubeLibs = [ - 'youtube-ext', - 'ytdl-core', - '@distube/ytdl-core', - 'play-dl', - 'yt-stream' - // add more to the list if you have any -]; - -const ERR_NO_YT_LIB = new Error(`Could not load youtube library. Install one of ${YouTubeLibs.map((lib) => `"${lib}"`).join(', ')}`); - -// forced lib -const forcedLib = process.env.DP_FORCE_YTDL_MOD; -if (forcedLib) YouTubeLibs.unshift(...forcedLib.split(',')); - -export type StreamFN = ( - q: string, - ext: BaseExtractor, - demuxable?: boolean -) => Promise< - | import('stream').Readable - | string - | { - stream: import('stream').Readable; - $fmt: string; - } ->; - -let httpAgent: http.Agent, httpsAgent: https.Agent; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function loadYtdl(options?: any, force = false) { - if (factory && !force) return factory; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let lib: any, _ytLibName: string, _stream: StreamFN; - - for (const ytlib of YouTubeLibs) { - lib = await import(ytlib).then( - (m) => m, - () => null - ); - if (!lib) continue; - lib = lib.default || lib; - _ytLibName = ytlib; - break; - } - - if (lib) { - const isYtdl = ['ytdl-core'].some((lib) => lib === _ytLibName); - - const hlsRegex = /\/manifest\/hls_(variant|playlist)\//; - _stream = async (query, extractor, demuxable = false) => { - const planner = extractor.context.player.routePlanner; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const applyPlannerConfig = (opt: any, applyAgents = false) => { - if (planner) { - try { - const { ip, family } = planner.getIP(); - - if (!applyAgents) { - opt.requestOptions.localAddress = ip; - opt.requestOptions.family = family; - } else { - const options = opt?.requestOptions || {}; - - options.localAddress = ip; - options.family = family; - - if (!httpAgent) httpAgent = new http.Agent(options); - if (!httpsAgent) httpsAgent = new https.Agent(options); - - return Object.assign(opt, { - requestOptions: options, - httpAgent, - httpsAgent - }); - } - } catch { - // - } - } - - return opt; - }; - - if (_ytLibName === 'youtube-ext') { - const dl = lib as typeof import('youtube-ext'); - const opt = applyPlannerConfig( - { - ...options, - requestOptions: options?.requestOptions || {} - }, - true - ); - - const info = await dl.videoInfo(query, opt); - const videoFormats = await dl.getFormats(info.stream, opt); - - if (demuxable) { - const demuxableFormat = - info.duration.lengthSec != '0' - ? videoFormats.find((fmt) => { - return /audio\/webm; codecs="opus"/.test(fmt.mimeType || '') && fmt.audioSampleRate == '48000'; - }) - : null; - - if (demuxableFormat) { - return { - stream: await dl.getReadableStream(demuxableFormat, opt), - $fmt: 'webm/opus' - }; - } - } - - const formats = videoFormats - .filter((format) => { - if (!format.url) return false; - if (info.isLive) return dl.utils.isHlsContentURL(format.url) && format.url.endsWith('.m3u8'); - return typeof format.bitrate === 'number'; - }) - .sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - - const fmt = formats.find((format) => !format.qualityLabel) || formats.sort((a, b) => Number(a.bitrate) - Number(b.bitrate))[0]; - const url = fmt?.url; - if (!url) throw new Error(`Failed to parse stream url for ${query}`); - return url; - } else if (isYtdl) { - const dl = lib as typeof import('ytdl-core'); - const info = await dl.getInfo(query, applyPlannerConfig(options)); - - if (demuxable) { - const filter = (format: import('ytdl-core').videoFormat) => { - return format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate == '48000'; - }; - - const format = info.formats.find(filter); - - if (format && info.videoDetails.lengthSeconds != '0') { - return { - stream: dl.downloadFromInfo(info, { - ...applyPlannerConfig(options), - filter - }), - $fmt: 'webm/opus' - }; - } - } - - const formats = info.formats - .filter((format) => { - return info.videoDetails.isLiveContent ? format.isHLS && format.hasAudio : format.hasAudio; - }) - .sort((a, b) => Number(b.audioBitrate) - Number(a.audioBitrate) || Number(a.bitrate) - Number(b.bitrate)); - - const fmt = formats.find((format) => !format.hasVideo) || formats.sort((a, b) => Number(a.bitrate) - Number(b.bitrate))[0]; - const url = fmt?.url; - if (!url) throw new Error(`Failed to parse stream url for ${query}`); - return url; - // return dl(query, this.context.player.options.ytdlOptions); - } else if (_ytLibName === '@distube/ytdl-core') { - const dl = lib as typeof import('@distube/ytdl-core'); - let opt: any; // eslint-disable-line @typescript-eslint/no-explicit-any - - if (planner) { - opt = { - localAddress: planner.getIP().ip, - autoSelectFamily: true - }; - } - - const cookie = options?.requestOptions?.headers?.cookie; - - const agent = dl.createAgent(Array.isArray(cookie) ? cookie : undefined, opt); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const reqOpt: any = { - agent - }; - - if (cookie && !Array.isArray(cookie)) { - reqOpt.requestOptions = { - headers: { - cookie - } - }; - } - - const info = await dl.getInfo(query, reqOpt); - - if (demuxable) { - const filter = (format: import('@distube/ytdl-core').videoFormat) => { - return format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate == '48000'; - }; - - const format = info.formats.find(filter); - - if (format && info.videoDetails.lengthSeconds != '0') { - return { - stream: dl.downloadFromInfo(info, { - ...applyPlannerConfig(options), - filter - }), - $fmt: 'webm/opus' - }; - } - } - - const formats = info.formats - .filter((format) => { - return info.videoDetails.isLiveContent ? format.isHLS && format.hasAudio : format.hasAudio; - }) - .sort((a, b) => Number(b.audioBitrate) - Number(a.audioBitrate) || Number(a.bitrate) - Number(b.bitrate)); - - const fmt = formats.find((format) => !format.hasVideo) || formats.sort((a, b) => Number(a.bitrate) - Number(b.bitrate))[0]; - const url = fmt?.url; - if (!url) throw new Error(`Failed to parse stream url for ${query}`); - return url; - } else if (_ytLibName === 'play-dl') { - const dl = lib as typeof import('play-dl'); - - if (typeof options?.requestOptions?.headers?.cookie === 'string') { - dl.setToken({ - youtube: { - cookie: options.requestOptions.headers.cookie - } - }); - } - const info = await dl.video_info(query); - - if (demuxable) { - try { - const stream = await dl.stream(query, { - discordPlayerCompatibility: false - }); - - return { - stream: stream.stream, - $fmt: stream.type as string - }; - } catch { - // - } - } - - const formats = info.format - .filter((format) => { - if (!format.url) return false; - if (info.video_details.live) return (hlsRegex.test(format.url) && typeof format.bitrate === 'number') || (hlsRegex.test(format.url) && format.url.endsWith('.m3u8')); - return typeof format.bitrate === 'number'; - }) - .sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); - - const fmt = formats.find((format) => !format.qualityLabel) || formats.sort((a, b) => Number(a.bitrate) - Number(b.bitrate))[0]; - const url = fmt?.url; - if (!url) throw new Error(`Failed to parse stream url for ${query}`); - return url; - } else if (_ytLibName === 'yt-stream') { - const dl = lib as typeof import('yt-stream'); - - const cookie = options?.requestOptions?.headers?.cookie; - - if (cookie && typeof cookie === 'string') dl.cookie = cookie; - - // @ts-ignore Default lib did not provide types for this function - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const decipher: any = await import('yt-stream/src/stream/decipher.js'); - - const info = await dl.getInfo(query); - - info.formats = await decipher?.format_decipher(info.formats, info.html5player); - - // @ts-ignore The lib did not provide ts support - const url = info.formats.filter((val) => val.mimeType.startsWith('audio') && val.audioQuality !== 'AUDIO_QUALITY_LOW').map((val) => val.url) as Array; - - if (url.length !== 0) return url[0]; - - // @ts-ignore The lib did not provide ts support - return info.formats.filter((val) => val.mimeType.startsWith('audio')).map((val) => val.url)[0] as string; - } else { - throw ERR_NO_YT_LIB; - } - }; - } else { - throw ERR_NO_YT_LIB; - } - - factory = { name: _ytLibName!, stream: _stream, lib }; - return factory; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function makeYTSearch(query: string, opt: any) { - const res = await YouTube.search(query, { - type: 'video', - safeSearch: opt?.safeSearch, - requestOptions: opt - }).catch(() => { - // - }); - - return res || []; -} - -export async function makeSCSearch(query: string) { - const { instance } = SoundCloudExtractor; - if (!instance?.internal) return []; - - let data: SoundCloud.SoundcloudTrackV2[]; - - try { - const info = await instance.internal.tracks.searchV2({ - q: query, - limit: 5 - }); - - data = info.collection; - } catch { - // fallback - const info = await instance.internal.tracks.searchAlt(query); - - data = info; - } - - return filterSoundCloudPreviews(data); -} - -export async function pullYTMetadata(ext: BaseExtractor, info: Track) { - const meta = await makeYTSearch(ext.createBridgeQuery(info), 'video') - .then((r) => r[0]) - .catch(() => null); - - return meta; -} - -export async function pullSCMetadata(ext: BaseExtractor, info: Track) { - const meta = await makeSCSearch(ext.createBridgeQuery(info)) - .then((r) => r[0]) - .catch(() => null); - - return meta; -} - -export function filterSoundCloudPreviews(tracks: SoundCloud.SoundcloudTrackV2[]): SoundCloud.SoundcloudTrackV2[] { - const filtered = tracks.filter((t) => { - if (typeof t.policy === 'string') return t.policy.toUpperCase() === 'ALLOW'; - return !(t.duration === 30_000 && t.full_duration > 30_000); - }); - - const result = filtered.length > 0 ? filtered : tracks; - - return result; -} diff --git a/packages/extractor/src/extractors/index.ts b/packages/extractor/src/extractors/index.ts deleted file mode 100644 index b27bcc4ed7..0000000000 --- a/packages/extractor/src/extractors/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './SoundCloudExtractor'; -export * from './YoutubeExtractor'; -export * from './LyricsExtractor'; -export * from './VimeoExtractor'; -export * from './ReverbnationExtractor'; -export * from './AttachmentExtractor'; -export * from './AppleMusicExtractor'; -export * from './SpotifyExtractor'; -export * from './BridgedExtractor'; -export * from './common/helper'; -export * from './common/BridgeProvider'; diff --git a/packages/extractor/src/index.ts b/packages/extractor/src/index.ts deleted file mode 100644 index c2ea6aeeaa..0000000000 --- a/packages/extractor/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './extractors'; -export * as Internal from './internal'; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/extractor/src/internal/AppleMusic.ts b/packages/extractor/src/internal/AppleMusic.ts deleted file mode 100644 index 7139c4be3c..0000000000 --- a/packages/extractor/src/internal/AppleMusic.ts +++ /dev/null @@ -1,287 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { QueryResolver } from 'discord-player'; -import { parse, HTMLElement } from 'node-html-parser'; -import { UA, fetch } from '../extractors/common/helper'; - -function getHTML(link: string): Promise { - return fetch(link, { - headers: { - 'User-Agent': UA - } - }) - .then((r) => r.text()) - .then( - (txt) => parse(txt), - () => null - ); -} - -function makeImage({ height, url, width, ext = 'jpg' }: { url: string; width: number; height: number; ext?: string }) { - return url.replace('{w}', `${width}`).replace('{h}', `${height}`).replace('{f}', ext); -} - -function parseDuration(d: string) { - const r = (name: string, unit: string) => `((?<${name}>-?\\d*[\\.,]?\\d+)${unit})?`; - const regex = new RegExp( - [ - '(?-)?P', - r('years', 'Y'), - r('months', 'M'), - r('weeks', 'W'), - r('days', 'D'), - '(T', - r('hours', 'H'), - r('minutes', 'M'), - r('seconds', 'S'), - ')?' // end optional time - ].join('') - ); - const test = regex.exec(d); - if (!test || !test.groups) return '0:00'; - - const dur = [test.groups.years, test.groups.months, test.groups.weeks, test.groups.days, test.groups.hours, test.groups.minutes, test.groups.seconds]; - - return ( - dur - .filter((r, i, a) => !!r || i > a.length - 2) - .map((m, i) => { - if (!m) m = '0'; - return i < 1 ? m : m.padStart(2, '0'); - }) - .join(':') || '0:00' - ); -} - -export class AppleMusic { - public constructor() { - return AppleMusic; - } - - public static async search(query: string) { - try { - const url = `https://music.apple.com/us/search?term=${encodeURIComponent(query)}`; - const node = await getHTML(url); - if (!node) return []; - - const rawData = node.getElementById('serialized-server-data'); - if (!rawData) return []; - - const data = JSON.parse(rawData.innerText)[0].data.sections; - const tracks = data.find((s: any) => s.itemKind === 'trackLockup')?.items; - if (!tracks) return []; - - return tracks.map((track: any) => ({ - id: track.contentDescriptor.identifiers.storeAdamID, - duration: track.duration || '0:00', - title: track.title, - url: track.contentDescriptor.url, - thumbnail: track?.artwork?.dictionary - ? makeImage({ - url: track.artwork.dictionary.url, - height: track.artwork.dictionary.height, - width: track.artwork.dictionary.width - }) - : 'https://music.apple.com/assets/favicon/favicon-180.png', - artist: { - name: track.subtitleLinks?.[0]?.title ?? 'Unknown Artist' - } - })); - } catch { - return []; - } - } - - public static async getSongInfoFallback(res: HTMLElement, name: string, id: string, link: string) { - try { - const metaTags = res.getElementsByTagName('meta'); - if (!metaTags.length) return null; - - const title = metaTags.find((r) => r.getAttribute('name') === 'apple:title')?.getAttribute('content') || res.querySelector('title')?.innerText || name; - const contentId = metaTags.find((r) => r.getAttribute('name') === 'apple:content_id')?.getAttribute('content') || id; - const durationRaw = metaTags.find((r) => r.getAttribute('property') === 'music:song:duration')?.getAttribute('content'); - - const song = { - id: contentId, - duration: durationRaw - ? parseDuration(durationRaw) - : metaTags - .find((m) => m.getAttribute('name') === 'apple:description') - ?.textContent.split('Duration: ')?.[1] - .split('"')?.[0] || '0:00', - title, - url: link, - thumbnail: - metaTags.find((r) => ['og:image:secure_url', 'og:image'].includes(r.getAttribute('property')!))?.getAttribute('content') || - 'https://music.apple.com/assets/favicon/favicon-180.png', - artist: { - name: res.querySelector('.song-subtitles__artists>a')?.textContent?.trim() || 'Apple Music' - } - }; - - return song; - } catch { - return null; - } - } - - public static async getSongInfo(link: string) { - if (!QueryResolver.regex.appleMusicSongRegex.test(link)) { - return null; - } - - const url = new URL(link); - const id = url.searchParams.get('i'); - const name = url.pathname.split('album/')[1]?.split('/')[0]; - - if (!id || !name) return null; - - const res = await getHTML(`https://music.apple.com/us/song/${name}/${id}`); - if (!res) return null; - - try { - const datasrc = - res.getElementById('serialized-server-data')?.innerText || res.innerText.split('')?.[0]; - if (!datasrc) throw 'not found'; - const data = JSON.parse(datasrc)[0].data.seoData; - const song = data.ogSongs[0]?.attributes; - - return { - id: data.ogSongs[0]?.id || data.appleContentId || id, - duration: song?.durationInMillis || '0:00', - title: song?.name || data.appleTitle, - url: song?.url || data.url || link, - thumbnail: song?.artwork - ? makeImage({ - url: song.artwork.url, - height: song.artwork.height, - width: song.artwork.width - }) - : data.artworkUrl - ? makeImage({ - height: data.height, - width: data.width, - url: data.artworkUrl, - ext: data.fileType || 'jpg' - }) - : 'https://music.apple.com/assets/favicon/favicon-180.png', - artist: { - name: song?.artistName || data.socialTitle || 'Apple Music' - } - }; - } catch { - return this.getSongInfoFallback(res, name, id, link); - } - } - - public static async getPlaylistInfo(link: string) { - if (!QueryResolver.regex.appleMusicPlaylistRegex.test(link)) { - return null; - } - - const res = await getHTML(link); - if (!res) return null; - - try { - const datasrc = - res.getElementById('serialized-server-data')?.innerText || res.innerText.split('')?.[0]; - if (!datasrc) throw 'not found'; - const pl = JSON.parse(datasrc)[0].data.seoData; - const thumbnail = pl.artworkUrl - ? makeImage({ - height: pl.height, - width: pl.width, - url: pl.artworkUrl, - ext: pl.fileType || 'jpg' - }) - : 'https://music.apple.com/assets/favicon/favicon-180.png'; - return { - id: pl.appleContentId, - title: pl.appleTitle, - thumbnail, - artist: { - name: pl.ogSongs?.[0]?.attributes?.artistName || 'Apple Music' - }, - url: pl.url, - tracks: - // eslint-disable-next-line - pl.ogSongs?.map((m: any) => { - const song = m.attributes; - return { - id: m.id, - duration: song.durationInMillis || '0:00', - title: song.name, - url: song.url, - thumbnail: song.artwork - ? makeImage({ - url: song.artwork.url, - height: song.artwork.height, - width: song.artwork.width - }) - : thumbnail, - artist: { - name: song.artistName || 'Apple Music' - } - }; - }) || [] - }; - } catch { - return null; - } - } - - public static async getAlbumInfo(link: string) { - if (!QueryResolver.regex.appleMusicAlbumRegex.test(link)) { - return null; - } - - const res = await getHTML(link); - if (!res) return null; - - try { - const datasrc = - res.getElementById('serialized-server-data')?.innerText || res.innerText.split('')?.[0]; - if (!datasrc) throw 'not found'; - const pl = JSON.parse(datasrc)[0].data.seoData; - const thumbnail = pl.artworkUrl - ? makeImage({ - height: pl.height, - width: pl.width, - url: pl.artworkUrl, - ext: pl.fileType || 'jpg' - }) - : 'https://music.apple.com/assets/favicon/favicon-180.png'; - return { - id: pl.appleContentId, - title: pl.appleTitle, - thumbnail, - artist: { - name: pl.ogSongs?.[0]?.attributes?.artistName || 'Apple Music' - }, - url: pl.url, - tracks: - // eslint-disable-next-line - pl.ogSongs?.map((m: any) => { - const song = m.attributes; - return { - id: m.id, - duration: song.durationInMillis || '0:00', - title: song.name, - url: song.url, - thumbnail: song.artwork - ? makeImage({ - url: song.artwork.url, - height: song.artwork.height, - width: song.artwork.width - }) - : thumbnail, - artist: { - name: song.artistName || 'Apple Music' - } - }; - }) || [] - }; - } catch { - return null; - } - } -} diff --git a/packages/extractor/src/internal/Spotify.ts b/packages/extractor/src/internal/Spotify.ts deleted file mode 100644 index a950a240af..0000000000 --- a/packages/extractor/src/internal/Spotify.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { fetch, UA } from '../extractors'; - -const SP_ANON_TOKEN_URL = 'https://open.spotify.com/get_access_token?reason=transport&productType=embed'; -const SP_ACCESS_TOKEN_URL = 'https://accounts.spotify.com/api/token?grant_type=client_credentials'; -const SP_BASE = 'https://api.spotify.com/v1'; - -interface SP_ACCESS_TOKEN { - token: string; - expiresAfter: number; - type: 'Bearer'; -} - -export class SpotifyAPI { - public accessToken: SP_ACCESS_TOKEN | null = null; - - public constructor( - public credentials: { clientId: string | null; clientSecret: string | null } = { - clientId: null, - clientSecret: null - } - ) {} - - public get authorizationKey() { - if (!this.credentials.clientId || !this.credentials.clientSecret) return null; - return Buffer.from(`${this.credentials.clientId}:${this.credentials.clientSecret}`).toString('base64'); - } - - public async requestToken() { - const key = this.authorizationKey; - - if (!key) return await this.requestAnonymousToken(); - - try { - const res = await fetch(SP_ACCESS_TOKEN_URL, { - method: 'POST', - headers: { - 'User-Agent': UA, - Authorization: `Basic ${key}`, - 'Content-Type': 'application/json' - } - }); - - const body = await res.json(); - - if (!body.access_token) throw 'no token'; - - const data = { - token: body.access_token as string, - expiresAfter: body.expires_in as number, - type: 'Bearer' as const - }; - - return (this.accessToken = data); - } catch { - return await this.requestAnonymousToken(); - } - } - - public async requestAnonymousToken() { - try { - const res = await fetch(SP_ANON_TOKEN_URL, { - headers: { - 'User-Agent': UA, - 'Content-Type': 'application/json' - } - }); - - if (!res.ok) throw 'not_ok'; - - const body = await res.json(); - - if (!body.accessToken) throw 'no_access_token'; - - const data = { - token: body.accessToken as string, - expiresAfter: body.accessTokenExpirationTimestampMs as number, - type: 'Bearer' as const - }; - - return (this.accessToken = data); - } catch { - return null; - } - } - - public isTokenExpired() { - if (!this.accessToken) return true; - return Date.now() > this.accessToken.expiresAfter; - } - - public async search(query: string) { - try { - // req - if (this.isTokenExpired()) await this.requestToken(); - // failed - if (!this.accessToken) return null; - - const res = await fetch(`${SP_BASE}/search/?q=${encodeURIComponent(query)}&type=track&market=US`, { - headers: { - 'User-Agent': UA, - Authorization: `${this.accessToken.type} ${this.accessToken.token}`, - 'Content-Type': 'application/json' - } - }); - - if (!res.ok) return null; - - const data: { tracks: { items: SpotifyTrack[] } } = await res.json(); - - return data.tracks.items.map((m) => ({ - title: m.name, - duration: m.duration_ms, - artist: m.artists.map((m) => m.name).join(', '), - url: m.external_urls?.spotify || `https://open.spotify.com/track/${m.id}`, - thumbnail: m.album.images?.[0]?.url || null - })); - } catch { - return null; - } - } - - public async getPlaylist(id: string) { - try { - // req - if (this.isTokenExpired()) await this.requestToken(); - // failed - if (!this.accessToken) return null; - - const res = await fetch(`${SP_BASE}/playlists/${id}?market=US`, { - headers: { - 'User-Agent': UA, - Authorization: `${this.accessToken.type} ${this.accessToken.token}`, - 'Content-Type': 'application/json' - } - }); - if (!res.ok) return null; - - const data: { - external_urls: { spotify: string }; - owner: { display_name: string }; - id: string; - name: string; - images: { url: string }[]; - tracks: { - items: { track: SpotifyTrack }[]; - next?: string; - }; - } = await res.json(); - - if (!data.tracks.items.length) return null; - - const t: { track: SpotifyTrack }[] = data.tracks.items; - - let next: string | undefined = data.tracks.next; - - while (typeof next === 'string') { - try { - const res = await fetch(next, { - headers: { - 'User-Agent': UA, - Authorization: `${this.accessToken.type} ${this.accessToken.token}`, - 'Content-Type': 'application/json' - } - }); - if (!res.ok) break; - const nextPage: { items: { track: SpotifyTrack }[]; next?: string } = await res.json(); - - t.push(...nextPage.items); - next = nextPage.next; - - if (!next) break; - } catch { - break; - } - } - - const tracks = t.map(({ track: m }) => ({ - title: m.name, - duration: m.duration_ms, - artist: m.artists.map((m) => m.name).join(', '), - url: m.external_urls?.spotify || `https://open.spotify.com/track/${m.id}`, - thumbnail: m.album.images?.[0]?.url || null - })); - - if (!tracks.length) return null; - return { - name: data.name, - author: data.owner.display_name, - thumbnail: data.images?.[0]?.url || null, - id: data.id, - url: data.external_urls.spotify || `https://open.spotify.com/playlist/${id}`, - tracks - }; - } catch { - return null; - } - } - - public async getAlbum(id: string) { - try { - // req - if (this.isTokenExpired()) await this.requestToken(); - // failed - if (!this.accessToken) return null; - - const res = await fetch(`${SP_BASE}/albums/${id}?market=US`, { - headers: { - 'User-Agent': UA, - Authorization: `${this.accessToken.type} ${this.accessToken.token}`, - 'Content-Type': 'application/json' - } - }); - if (!res.ok) return null; - - const data: { - external_urls: { spotify: string }; - artists: { name: string }[]; - id: string; - name: string; - images: { url: string }[]; - tracks: { - items: SpotifyTrack[]; - next?: string; - }; - } = await res.json(); - - if (!data.tracks.items.length) return null; - - const t: SpotifyTrack[] = data.tracks.items; - - let next: string | undefined = data.tracks.next; - - while (typeof next === 'string') { - try { - const res = await fetch(next, { - headers: { - 'User-Agent': UA, - Authorization: `${this.accessToken.type} ${this.accessToken.token}`, - 'Content-Type': 'application/json' - } - }); - if (!res.ok) break; - const nextPage: { items: SpotifyTrack[]; next?: string } = await res.json(); - - t.push(...nextPage.items); - next = nextPage.next; - - if (!next) break; - } catch { - break; - } - } - - const tracks = t.map((m) => ({ - title: m.name, - duration: m.duration_ms, - artist: m.artists.map((m) => m.name).join(', '), - url: m.external_urls?.spotify || `https://open.spotify.com/track/${m.id}`, - thumbnail: data.images?.[0]?.url || null - })); - - if (!tracks.length) return null; - return { - name: data.name, - author: data.artists.map((m) => m.name).join(', '), - thumbnail: data.images?.[0]?.url || null, - id: data.id, - url: data.external_urls.spotify || `https://open.spotify.com/album/${id}`, - tracks - }; - } catch { - return null; - } - } -} - -export interface SpotifyTrack { - album: { - images: { - height: number; - url: string; - width: number; - }[]; - }; - artists: { - id: string; - name: string; - }[]; - duration_ms: number; - explicit: boolean; - external_urls: { spotify: string }; - id: string; - name: string; -} diff --git a/packages/extractor/src/internal/Vimeo.ts b/packages/extractor/src/internal/Vimeo.ts deleted file mode 100644 index c4bf70ca2b..0000000000 --- a/packages/extractor/src/internal/Vimeo.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Readable } from 'stream'; -import http from 'http'; -import https from 'https'; -import { fetch } from '../extractors/common/helper'; - -class Vimeo { - constructor() { - throw new Error(`The ${this.constructor.name} class may not be instantiated!`); - } - - /** - * @typedef {Readable} Readable - */ - - /** - * Downloads from vimeo - * @param {number} id Vimeo video id - * @returns {Promise} - */ - static download(id: number | string): Promise { - return new Promise(async (resolve) => { - const info = await Vimeo.getInfo(id); - if (!info) return null; - - const downloader = info.stream.startsWith('https://') ? https : http; - - downloader.get(info.stream, (res) => { - resolve(res); - }); - }); - } - - /** - * Returns video info - * @param {number} id Video id - */ - static async getInfo(id: number | string): Promise { - if (!id) throw new Error('Invalid id'); - const url = `https://player.vimeo.com/video/${id}`; - - try { - const res = await fetch(url); - const data = await res.text(); - const json = JSON.parse(data.split('window.playerConfig =')[1].split(';')[0].trim()); - - const obj = { - id: json.video.id, - duration: json.video.duration * 1000, - title: json.video.title, - url: json.video.url, - thumbnail: json.video.thumbs['1280'] || json.video.thumbs.base, - stream: json.request.files.progressive[0].url, - author: { - id: json.video.owner.id, - name: json.video.owner.name, - url: json.video.owner.url, - avatar: json.video.owner.img_2x || json.video.owner.img - } - }; - - return obj; - } catch { - return null; - } - } -} - -export interface VimeoInfo { - id: number; - duration: number; - title: string; - url: string; - thumbnail: string; - stream: string; - author: { - id: number; - name: string; - url: string; - avatar: string; - }; -} - -export { Vimeo }; diff --git a/packages/extractor/src/internal/downloader.ts b/packages/extractor/src/internal/downloader.ts deleted file mode 100644 index 6204c63369..0000000000 --- a/packages/extractor/src/internal/downloader.ts +++ /dev/null @@ -1,11 +0,0 @@ -import http, { RequestOptions } from 'http'; -import https from 'https'; -import { Readable } from 'stream'; - -export function downloadStream(url: string, opts: RequestOptions = {}) { - return new Promise((resolve, reject) => { - const lib = url.startsWith('http://') ? http : https; - - lib.get(url, opts, (res) => resolve(res)).once('error', reject); - }); -} diff --git a/packages/extractor/src/internal/index.ts b/packages/extractor/src/internal/index.ts deleted file mode 100644 index a46b811c43..0000000000 --- a/packages/extractor/src/internal/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './AppleMusic'; -export * from './Vimeo'; -export * from './downloader'; -export * from './Spotify'; diff --git a/packages/extractor/src/types/reverbnation.d.ts b/packages/extractor/src/types/reverbnation.d.ts deleted file mode 100644 index 822d35a863..0000000000 --- a/packages/extractor/src/types/reverbnation.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -declare module 'reverbnation-scraper' { - import internal from 'stream'; - - class Song { - public title: string; - public id: number; - public image: string; - public thumbnail: string; - public duration: number; - public bitrate: number; - public lyrics: string; - public streamURL: string; - public public: boolean; - public url: string; - public contextImage: { - original: string; - blur: string; - colors: { - average_lightness: number; - greyscale: boolean; - vibrant: string; - light_vibrant: string; - dark_vibrant: string; - muted: string; - light_muted: string; - dark_muted: string; - }; - source: string; - } | null; - } - - class Artist { - public id: number; - public name: string; - public profile: string; - public type: string; - public avatar: string; - public thumbnail: string; - public bio: string; - public genres: string[]; - public location: { - city: string; - state: string | null; - country: string; - }; - } - - export type ReverbnationInfo = Song & { - artist: Artist; - songs: Song[]; - }; - - export function getInfo(url: string): Promise; - - function rvdl(url: string): Promise; - - export = rvdl; -} diff --git a/packages/extractor/src/types/spotify.d.ts b/packages/extractor/src/types/spotify.d.ts deleted file mode 100644 index 21b6e8f912..0000000000 --- a/packages/extractor/src/types/spotify.d.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -declare module 'spotify-url-info' { - export interface Spotify { - getPreview(url: string, opts?: RequestInit): Promise; - getTracks(url: string, opts?: RequestInit): Promise; - getDetails(url: string, opts?: RequestInit): Promise; - getData(url: string, opts?: RequestInit): Promise; - getLink(data: any): string; - } - - function spotifyUrlInfo(fetch: any): Spotify; - - export interface SpotifySong { - type: 'track'; - name: string; - uri: string; - id: string; - title: string; - artists: { - name: string; - uri: string; - }[]; - coverArt: { - extractedColors: { - colorDark: { - hex: string; - }; - colorLight: { - hex: string; - }; - }; - sources: { - url: string; - width: number; - height: number; - }[]; - }; - releaseDate: { - isoString: string; - }; - duration: number; - maxDuration: number; - isPlayable: boolean; - isExplicit: boolean; - audioPreview: { - url: string; - format: string; - }; - hasVideo: boolean; - relatedEntityUri: string; - } - - export interface SpotifyAlbum { - type: 'album'; - name: string; - uri: string; - id: string; - title: string; - subtitle: string; - coverArt: { - extractedColors: { - colorDark: { - hex: string; - }; - }; - sources: { - height: number; - width: number; - url: string; - }[]; - }; - releaseDate: string; - duration: number; - maxDuration: number; - isPlayable: boolean; - isExplicit: boolean; - hasVideo: boolean; - relatedEntityUri: string; - trackList: { - uri: string; - uid: string; - title: string; - subtitle: string; - isExplicit: boolean; - duration: number; - isPlayable: boolean; - audioPreview: { - format: string; - url: string; - }; - }[]; - } - - export interface SpotifyPlaylist { - type: 'playlist'; - name: string; - uri: string; - id: string; - title: string; - subtitle: string; - coverArt: { - extractedColors: { - colorDark: { - hex: string; - }; - }; - sources: { - height: number; - width: number; - url: string; - }[]; - }; - releaseDate: string; - duration: number; - maxDuration: number; - isPlayable: boolean; - isExplicit: boolean; - hasVideo: boolean; - relatedEntityUri: string; - trackList: { - uri: string; - uid: string; - title: string; - subtitle: string; - isExplicit: boolean; - duration: number; - isPlayable: boolean; - audioPreview: { - format: string; - url: string; - }; - }[]; - } - - export = spotifyUrlInfo; -} diff --git a/packages/extractor/tsconfig.json b/packages/extractor/tsconfig.json deleted file mode 100644 index 7107d22e99..0000000000 --- a/packages/extractor/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "@discord-player/tsconfig/base.json", - "include": [ - "src/**/*" -, "../discord-player/src/utils/IPRotator.ts" ] -} \ No newline at end of file diff --git a/packages/extractor/tsup.config.ts b/packages/extractor/tsup.config.ts deleted file mode 100644 index e161ba4d4d..0000000000 --- a/packages/extractor/tsup.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '../../tsup.config'; -import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; - -export default defineConfig({ - esbuildPlugins: [esbuildPluginVersionInjector()] -}); diff --git a/packages/extractor/typedoc.json b/packages/extractor/typedoc.json deleted file mode 100644 index b3eddf3024..0000000000 --- a/packages/extractor/typedoc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entryPoints": ["src/index.ts"], - "excludePrivate": true, - "excludeExternals": true -} diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 2b802aa76f..6fa7319139 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -20,4 +20,4 @@ "devDependencies": { "typescript": "^5.2.2" } -} \ No newline at end of file +} diff --git a/packages/voice/LICENSE b/packages/voice/LICENSE deleted file mode 100644 index fe07fc7364..0000000000 --- a/packages/voice/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Androz2091 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/voice/README.md b/packages/voice/README.md deleted file mode 100644 index 99c3658124..0000000000 --- a/packages/voice/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# `@discord-player/voice` - -A high level framework for Discord VoIP client. - -> WIP - -## Installation - -```sh -$ yarn add @discord-player/voice -``` - -## Example - -```js -import pkg from '@discord-player/voice'; - -// other code -``` diff --git a/packages/voice/__test__/sum.spec.ts b/packages/voice/__test__/sum.spec.ts deleted file mode 100644 index 6d3cd1af62..0000000000 --- a/packages/voice/__test__/sum.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { add } from '../src'; -import { describe, it, expect } from 'vitest'; - -describe('Sum', () => { - it('should add two numbers', () => { - expect(add(2, 2)).toBe(4); - }); -}); diff --git a/packages/voice/package.json b/packages/voice/package.json deleted file mode 100644 index e5e8c8c13c..0000000000 --- a/packages/voice/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@discord-player/voice", - "version": "0.1.0", - "description": "A high level framework for Discord VoIP client", - "keywords": [ - "discord-player", - "music", - "bot", - "discord.js", - "javascript", - "voip", - "lavalink", - "lavaplayer" - ], - "author": "Androz2091 ", - "homepage": "https://discord-player.js.org", - "license": "MIT", - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/Androz2091/discord-player.git" - }, - "scripts": { - "build": "tsup", - "build:check": "tsc --noEmit", - "lint": "eslint src --ext .ts --fix", - "test": "vitest", - "coverage": "vitest run --coverage" - }, - "bugs": { - "url": "https://github.com/Androz2091/discord-player/issues" - }, - "devDependencies": { - "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", - "vitest": "^0.34.6" - }, - "dependencies": { - "@discord-player/utils": "workspace:^", - "discord-voip": "^0.1.2" - } -} diff --git a/packages/voice/src/DiscordVoiceAdapter.ts b/packages/voice/src/DiscordVoiceAdapter.ts deleted file mode 100644 index c4609012d9..0000000000 --- a/packages/voice/src/DiscordVoiceAdapter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DiscordGatewayAdapterLibraryMethods } from 'discord-voip'; -import { GatewayVoiceServerUpdateDispatch, GatewayVoiceStateUpdateDispatch } from 'discord-api-types/v10'; -import { VoiceManager } from './VoiceManager'; - -export type VoiceAdapterData = DiscordGatewayAdapterLibraryMethods & { - id: string; -}; - -export type VoiceAdapterIncomingPayload = GatewayVoiceServerUpdateDispatch | GatewayVoiceStateUpdateDispatch; - -export class DiscordVoiceAdapter { - public constructor(public readonly manager: VoiceManager, public methods: VoiceAdapterData) {} - - public onPayload(data: VoiceAdapterIncomingPayload) { - void data; - } - - public sendPayload(data: unknown) { - // return this.manager.emit('payload', data); - void data; - } - - public destroy() { - this.manager.internalAdaptersCache.delete(this.methods.id); - } -} diff --git a/packages/voice/src/VoiceConnection.ts b/packages/voice/src/VoiceConnection.ts deleted file mode 100644 index ba20a6f884..0000000000 --- a/packages/voice/src/VoiceConnection.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DiscordVoiceConnection } from './common'; -import type { VoiceJoinConfig, VoiceManager } from './VoiceManager'; - -export class VoiceConnection { - public constructor(public readonly manager: VoiceManager, public readonly connection: DiscordVoiceConnection) {} - - public get channel() { - return this.connection.joinConfig.channelId; - } - - public get guild() { - return this.connection.joinConfig.guildId; - } - - public static async create(manager: VoiceManager, config: VoiceJoinConfig) { - void config; - // const connection = await DiscordJoinVoiceChannel({ - // ...config, - // adapterCreator: (methods) => { - // manager.adapter; - // } - // }); - - // return new VoiceConnection(manager, connection); - } -} diff --git a/packages/voice/src/VoiceManager.ts b/packages/voice/src/VoiceManager.ts deleted file mode 100644 index fd125ad9a8..0000000000 --- a/packages/voice/src/VoiceManager.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Collection } from '@discord-player/utils'; -import { DiscordGatewayAdapterImplementerMethods } from 'discord-voip'; -// import { DiscordVoiceAdapter } from './DiscordVoiceAdapter'; -import { VoiceConnection } from './VoiceConnection'; - -export interface VoiceJoinConfig { - channelId: string; - guildId: string; - group?: string; - selfDeaf?: boolean; - selfMute?: boolean; -} - -export class VoiceManager { - public connections = new Collection(); - // public adapter = new DiscordVoiceAdapter(this); - public internalAdaptersCache = new Collection(); - - public async join(config: VoiceJoinConfig) { - if (this.connections.has(config.channelId)) return this.connections.get(config.channelId)!; - const connection = await VoiceConnection.create(this, config); - // this.connections.set(connection.channel!, connection); - - return connection; - } -} diff --git a/packages/voice/src/common.ts b/packages/voice/src/common.ts deleted file mode 100644 index 05cdf561a3..0000000000 --- a/packages/voice/src/common.ts +++ /dev/null @@ -1 +0,0 @@ -export { AudioPlayer as DiscordAudioPlayer, VoiceConnection as DiscordVoiceConnection, joinVoiceChannel as DiscordJoinVoiceChannel } from 'discord-voip'; diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts deleted file mode 100644 index ac4ac60b7c..0000000000 --- a/packages/voice/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const add = (a: number, b: number) => { - return a + b; -}; - -// eslint-disable-next-line @typescript-eslint/no-inferrable-types -export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/voice/tsconfig.json b/packages/voice/tsconfig.json deleted file mode 100644 index 3c774c914c..0000000000 --- a/packages/voice/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "@discord-player/tsconfig/base.json", - "include": [ - "src/**/*" - ] -} \ No newline at end of file diff --git a/packages/voice/tsup.config.ts b/packages/voice/tsup.config.ts deleted file mode 100644 index e161ba4d4d..0000000000 --- a/packages/voice/tsup.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '../../tsup.config'; -import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; - -export default defineConfig({ - esbuildPlugins: [esbuildPluginVersionInjector()] -}); diff --git a/packages/voice/vitest.config.ts b/packages/voice/vitest.config.ts deleted file mode 100644 index e055dbaa74..0000000000 --- a/packages/voice/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - dir: `${__dirname}/__test__`, - passWithNoTests: true, - watch: false - } -}); diff --git a/turbo.json b/turbo.json index 64612e3659..b2d049cf7e 100644 --- a/turbo.json +++ b/turbo.json @@ -10,11 +10,6 @@ ".next/**" ] }, - "@discord-player/extractor#build": { - "dependsOn": [ - "discord-player#build" - ] - }, "docs": {}, "build:check": {}, "lint": {}, diff --git a/typedoc.json b/typedoc.json index 435c0ecdfe..b62629bd1d 100644 --- a/typedoc.json +++ b/typedoc.json @@ -5,7 +5,6 @@ "entryPoints": [ "packages/discord-player", "packages/equalizer", - "packages/extractor", "packages/utils", "packages/ffmpeg", "packages/opus" diff --git a/yarn.lock b/yarn.lock index db08e7c715..ac743124d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,42 +82,6 @@ __metadata: languageName: node linkType: hard -"@discord-player/adapter-local@workspace:packages/adapter-local": - version: 0.0.0-use.local - resolution: "@discord-player/adapter-local@workspace:packages/adapter-local" - dependencies: - "@discord-player/tsconfig": "workspace:^" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - languageName: unknown - linkType: soft - -"@discord-player/adapter-remote@workspace:packages/adapter-remote": - version: 0.0.0-use.local - resolution: "@discord-player/adapter-remote@workspace:packages/adapter-remote" - dependencies: - "@discord-player/tsconfig": "workspace:^" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - languageName: unknown - linkType: soft - -"@discord-player/core@workspace:packages/core": - version: 0.0.0-use.local - resolution: "@discord-player/core@workspace:packages/core" - dependencies: - "@discord-player/tsconfig": "workspace:^" - "@discord-player/utils": "workspace:^" - discord-api-types: "npm:^0.37.2" - discord-voip: "npm:^0.1.2" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - languageName: unknown - linkType: soft - "@discord-player/discord-player@workspace:.": version: 0.0.0-use.local resolution: "@discord-player/discord-player@workspace:." @@ -138,18 +102,6 @@ __metadata: languageName: unknown linkType: soft -"@discord-player/downloader@workspace:packages/downloader": - version: 0.0.0-use.local - resolution: "@discord-player/downloader@workspace:packages/downloader" - dependencies: - "@discord-player/tsconfig": "workspace:^" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - youtube-dl-exec: "npm:^2.1.11" - languageName: unknown - linkType: soft - "@discord-player/equalizer@workspace:^, @discord-player/equalizer@workspace:packages/equalizer": version: 0.0.0-use.local resolution: "@discord-player/equalizer@workspace:packages/equalizer" @@ -161,33 +113,6 @@ __metadata: languageName: unknown linkType: soft -"@discord-player/extractor@workspace:^, @discord-player/extractor@workspace:packages/extractor": - version: 0.0.0-use.local - resolution: "@discord-player/extractor@workspace:packages/extractor" - dependencies: - "@discord-player/tsconfig": "workspace:^" - "@distube/ytdl-core": "npm:^4.13.0" - "@types/node": "npm:^18.11.18" - discord-player: "workspace:^" - file-type: "npm:^16.5.4" - genius-lyrics: "npm:^4.4.6" - isomorphic-unfetch: "npm:^4.0.2" - mediaplex: "npm:^0.0.7" - node-html-parser: "npm:^6.1.4" - play-dl: "npm:^1.9.6" - reverbnation-scraper: "npm:^2.0.0" - soundcloud.ts: "npm:^0.5.2" - spotify-url-info: "npm:^3.2.6" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - youtube-ext: "npm:^1.1.14" - youtube-sr: "npm:^4.3.9" - yt-stream: "npm:^1.4.8" - ytdl-core: "npm:^4.11.4" - languageName: unknown - linkType: soft - "@discord-player/ffmpeg@npm:^0.1.0, @discord-player/ffmpeg@workspace:^, @discord-player/ffmpeg@workspace:packages/ffmpeg": version: 0.0.0-use.local resolution: "@discord-player/ffmpeg@workspace:packages/ffmpeg" @@ -200,7 +125,7 @@ __metadata: languageName: unknown linkType: soft -"@discord-player/opus@npm:^0.1.0, @discord-player/opus@npm:^0.1.2, @discord-player/opus@workspace:packages/opus": +"@discord-player/opus@npm:^0.1.2, @discord-player/opus@workspace:packages/opus": version: 0.0.0-use.local resolution: "@discord-player/opus@workspace:packages/opus" dependencies: @@ -232,19 +157,6 @@ __metadata: languageName: unknown linkType: soft -"@discord-player/voice@workspace:packages/voice": - version: 0.0.0-use.local - resolution: "@discord-player/voice@workspace:packages/voice" - dependencies: - "@discord-player/tsconfig": "workspace:^" - "@discord-player/utils": "workspace:^" - discord-voip: "npm:^0.1.2" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - languageName: unknown - linkType: soft - "@discordjs/builders@npm:^1.1.0": version: 1.1.0 resolution: "@discordjs/builders@npm:1.1.0" @@ -258,21 +170,6 @@ __metadata: languageName: node linkType: hard -"@discordjs/builders@npm:^1.6.3": - version: 1.6.3 - resolution: "@discordjs/builders@npm:1.6.3" - dependencies: - "@discordjs/formatters": "npm:^0.3.1" - "@discordjs/util": "npm:^0.3.1" - "@sapphire/shapeshift": "npm:^3.8.2" - discord-api-types: "npm:^0.37.41" - fast-deep-equal: "npm:^3.1.3" - ts-mixer: "npm:^6.0.3" - tslib: "npm:^2.5.0" - checksum: 10/1f9fa688c4e8a466eb53f2817c54aecb09e1d0c2a26ca6f309170d15c62ec382ba207d5838c75a58b497a03bbd743a4c2b07798d1658716f38d63336efb2f5fc - languageName: node - linkType: hard - "@discordjs/collection@npm:^1.0.1": version: 1.0.1 resolution: "@discordjs/collection@npm:1.0.1" @@ -287,51 +184,6 @@ __metadata: languageName: node linkType: hard -"@discordjs/collection@npm:^1.5.1": - version: 1.5.1 - resolution: "@discordjs/collection@npm:1.5.1" - checksum: 10/e7e7ee7eba078710785173f01ef1f530b66cde8521d14bec65f06bbfd11841741cc7e80e382a2c5e91ada5d1dcb1dc1f4db31adaa0790fb2ba9c1d7e9120cfef - languageName: node - linkType: hard - -"@discordjs/formatters@npm:^0.3.1": - version: 0.3.1 - resolution: "@discordjs/formatters@npm:0.3.1" - dependencies: - discord-api-types: "npm:^0.37.41" - checksum: 10/69e97aa42b0f9b0d7716763a0680cdb96674eeea5e82eed3d7efae5aa43b00be99498089a97021dd074a53aa8c2a87c6a6c2a74af9f03a550fb08e7c06140932 - languageName: node - linkType: hard - -"@discordjs/node-pre-gyp@npm:^0.4.5": - version: 0.4.5 - resolution: "@discordjs/node-pre-gyp@npm:0.4.5" - dependencies: - detect-libc: "npm:^2.0.0" - https-proxy-agent: "npm:^5.0.0" - make-dir: "npm:^3.1.0" - node-fetch: "npm:^2.6.7" - nopt: "npm:^5.0.0" - npmlog: "npm:^5.0.1" - rimraf: "npm:^3.0.2" - semver: "npm:^7.3.5" - tar: "npm:^6.1.11" - bin: - node-pre-gyp: bin/node-pre-gyp - checksum: 10/c612cbd50d2b7bbf3d31abc1d68f38361dc492010cdf706dd6a2c1e959456b632106840d254c7ffca0da014fb747e37c6983d4b6d8bb9914e5a921f22a7921df - languageName: node - linkType: hard - -"@discordjs/opus@npm:^0.9.0": - version: 0.9.0 - resolution: "@discordjs/opus@npm:0.9.0" - dependencies: - "@discordjs/node-pre-gyp": "npm:^0.4.5" - node-addon-api: "npm:^5.0.0" - checksum: 10/5b2d989c8ad950743a88c8e2141ec29bd9f075151df0d0479c47db3d33a4e9fae2620d34016e298323f0ea27a37a316fd1a6e071b7fba91e92b3186abc56c9c9 - languageName: node - linkType: hard - "@discordjs/rest@npm:^1.0.1": version: 1.0.1 resolution: "@discordjs/rest@npm:1.0.1" @@ -347,40 +199,6 @@ __metadata: languageName: node linkType: hard -"@discordjs/util@npm:^0.3.1": - version: 0.3.1 - resolution: "@discordjs/util@npm:0.3.1" - checksum: 10/3589b71f924eb174676d7c8fd9870bd9208163922e7dd3fd3f038f7f01f70b84c0dd12abb73262cb8064b1ff2ada1b9e5398a4ee6806283155e356bbf6e9cc6d - languageName: node - linkType: hard - -"@distube/ytdl-core@npm:^4.13.0": - version: 4.13.0 - resolution: "@distube/ytdl-core@npm:4.13.0" - dependencies: - http-cookie-agent: "npm:^5.0.4" - m3u8stream: "npm:^0.8.6" - sax: "npm:^1.2.4" - tough-cookie: "npm:^4.1.3" - undici: "npm:^5.23.0" - checksum: 10/3d2d0f57979714429eff546259ce7c556a34c035d5a236aea776dff2afe96b306bea715812b5684ce78bd135d8f9e35c6f25e2c88ebd019581708426bd3f901e - languageName: node - linkType: hard - -"@distube/ytdl-core@npm:^4.13.3": - version: 4.13.3 - resolution: "@distube/ytdl-core@npm:4.13.3" - dependencies: - http-cookie-agent: "npm:^5.0.4" - m3u8stream: "npm:^0.8.6" - miniget: "npm:^4.2.3" - sax: "npm:^1.2.4" - tough-cookie: "npm:^4.1.3" - undici: "npm:^5.25.2" - checksum: 10/b7c8080274346aeafa44d61204324d450c29dd8f000340e637467396a8ba0191984721ca83c564b3c2b221ce9b781bad09e7bd82e017b45bacdae718c3780877 - languageName: node - linkType: hard - "@edge-ui/react@npm:0.0.11": version: 0.0.11 resolution: "@edge-ui/react@npm:0.0.11" @@ -436,43 +254,6 @@ __metadata: languageName: node linkType: hard -"@esbuild-kit/cjs-loader@npm:^2.4.2": - version: 2.4.2 - resolution: "@esbuild-kit/cjs-loader@npm:2.4.2" - dependencies: - "@esbuild-kit/core-utils": "npm:^3.0.0" - get-tsconfig: "npm:^4.4.0" - checksum: 10/e346e339bfc7eff5c52c270fd0ec06a7f2341b624adfb69f84b7d83f119c35070420906f2761a0b4604e0a0ec90e35eaf12544585476c428ed6d6ee3b250c0fe - languageName: node - linkType: hard - -"@esbuild-kit/core-utils@npm:^3.0.0": - version: 3.1.0 - resolution: "@esbuild-kit/core-utils@npm:3.1.0" - dependencies: - esbuild: "npm:~0.17.6" - source-map-support: "npm:^0.5.21" - checksum: 10/a9b1702bcfe1232f2d89dee0ddd81430df95852abe3c18bf305b2bfdb380f689f8147b327c82728a92dfc3af43a59045bb37191665c383045293376c07665353 - languageName: node - linkType: hard - -"@esbuild-kit/esm-loader@npm:^2.5.5": - version: 2.5.5 - resolution: "@esbuild-kit/esm-loader@npm:2.5.5" - dependencies: - "@esbuild-kit/core-utils": "npm:^3.0.0" - get-tsconfig: "npm:^4.4.0" - checksum: 10/9d4a03ffc937fbec75a8456c3d45d7cdb1a65768416791a5720081753502bc9f485ba27942a46f564b12483b140a8a46c12433a4496430d93e4513e430484ec7 - languageName: node - linkType: hard - -"@esbuild/android-arm64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/android-arm64@npm:0.17.19" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm64@npm:0.18.20" @@ -487,13 +268,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/android-arm@npm:0.17.19" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm@npm:0.18.20" @@ -508,13 +282,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/android-x64@npm:0.17.19" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-x64@npm:0.18.20" @@ -529,13 +296,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/darwin-arm64@npm:0.17.19" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-arm64@npm:0.18.20" @@ -550,13 +310,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/darwin-x64@npm:0.17.19" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-x64@npm:0.18.20" @@ -571,13 +324,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/freebsd-arm64@npm:0.17.19" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-arm64@npm:0.18.20" @@ -592,13 +338,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/freebsd-x64@npm:0.17.19" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-x64@npm:0.18.20" @@ -613,13 +352,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-arm64@npm:0.17.19" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm64@npm:0.18.20" @@ -634,13 +366,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-arm@npm:0.17.19" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm@npm:0.18.20" @@ -655,13 +380,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-ia32@npm:0.17.19" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ia32@npm:0.18.20" @@ -676,13 +394,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-loong64@npm:0.17.19" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-loong64@npm:0.18.20" @@ -697,13 +408,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-mips64el@npm:0.17.19" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-mips64el@npm:0.18.20" @@ -718,13 +422,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-ppc64@npm:0.17.19" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ppc64@npm:0.18.20" @@ -739,13 +436,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-riscv64@npm:0.17.19" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-riscv64@npm:0.18.20" @@ -760,13 +450,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-s390x@npm:0.17.19" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-s390x@npm:0.18.20" @@ -781,13 +464,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/linux-x64@npm:0.17.19" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-x64@npm:0.18.20" @@ -802,13 +478,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/netbsd-x64@npm:0.17.19" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/netbsd-x64@npm:0.18.20" @@ -823,13 +492,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/openbsd-x64@npm:0.17.19" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/openbsd-x64@npm:0.18.20" @@ -844,13 +506,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/sunos-x64@npm:0.17.19" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/sunos-x64@npm:0.18.20" @@ -865,13 +520,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/win32-arm64@npm:0.17.19" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-arm64@npm:0.18.20" @@ -886,13 +534,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/win32-ia32@npm:0.17.19" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-ia32@npm:0.18.20" @@ -907,13 +548,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.17.19": - version: 0.17.19 - resolution: "@esbuild/win32-x64@npm:0.17.19" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-x64@npm:0.18.20" @@ -1001,13 +635,6 @@ __metadata: languageName: node linkType: hard -"@fastify/busboy@npm:^2.0.0": - version: 2.0.0 - resolution: "@fastify/busboy@npm:2.0.0" - checksum: 10/6a2366d06b82aac1069b8323792f76f7a8fca02533cb3745fcd218d8f0f953dc4dbef057287237414658cd43f8dede0846ef33398999e3dbe54ddaeefec71c0a - languageName: node - linkType: hard - "@floating-ui/core@npm:^1.3.1": version: 1.3.1 resolution: "@floating-ui/core@npm:1.3.1" @@ -1564,22 +1191,6 @@ __metadata: languageName: node linkType: hard -"@mole-inc/bin-wrapper@npm:^8.0.1": - version: 8.0.1 - resolution: "@mole-inc/bin-wrapper@npm:8.0.1" - dependencies: - bin-check: "npm:^4.1.0" - bin-version-check: "npm:^5.0.0" - content-disposition: "npm:^0.5.4" - ext-name: "npm:^5.0.0" - file-type: "npm:^17.1.6" - filenamify: "npm:^5.0.2" - got: "npm:^11.8.5" - os-filter-obj: "npm:^2.0.0" - checksum: 10/565df38f6f91fefe2e2540bf4357024fd912990aecb1873fd9bf21e6a667d9930c73e5cbc87a9aac0cf476b8dffc30a4e00ec97b62e713ef5c00d46823ea599d - languageName: node - linkType: hard - "@next/env@npm:13.4.7": version: 13.4.7 resolution: "@next/env@npm:13.4.7" @@ -3294,241 +2905,60 @@ __metadata: languageName: node linkType: hard -"@sapphire/discord-utilities@npm:^3.0.3": - version: 3.0.3 - resolution: "@sapphire/discord-utilities@npm:3.0.3" - dependencies: - discord-api-types: "npm:^0.37.41" - checksum: 10/2e7e3b8189ea7d3a376080bfebcd93f74569e201840f3c113006760c852a68e94adc7575e00e4ce82481fdd071cace8e0dbcfc1b8dd4184e6b002c1896cdbb15 +"@sapphire/result@npm:^2.6.0": + version: 2.6.0 + resolution: "@sapphire/result@npm:2.6.0" + checksum: 10/548cb7cf63f6d17a0937e2575d85104bb607e529d5b97bc71dabb5896094432e712a1f3e28e374565f069ccb123bd6799a42810af65daa53f02452000e184c65 languageName: node linkType: hard -"@sapphire/discord.js-utilities@npm:^6.0.3, @sapphire/discord.js-utilities@npm:^6.1.0": - version: 6.1.0 - resolution: "@sapphire/discord.js-utilities@npm:6.1.0" +"@sapphire/shapeshift@npm:^3.5.1": + version: 3.5.1 + resolution: "@sapphire/shapeshift@npm:3.5.1" dependencies: - "@sapphire/discord-utilities": "npm:^3.0.3" - "@sapphire/duration": "npm:^1.1.0" - "@sapphire/utilities": "npm:^3.11.1" - tslib: "npm:^2.5.0" - checksum: 10/745c65cadb3110809931b15da30d91c81c78846f296e269558967fd9c5d7fa1fdd36e67864f64ca935a918a9a593d8e039b6d9a5a872e1527ad65fc0243ebc63 + fast-deep-equal: "npm:^3.1.3" + lodash.uniqwith: "npm:^4.5.0" + checksum: 10/3791c9b17507eb864656520fcaba17dc660e39f3b2e08d2560438b60ba7752dbdc502ca6db378498f2961d5fe12ca6fae0b4c6e41cff06896d940525981d775b languageName: node linkType: hard -"@sapphire/duration@npm:^1.0.0, @sapphire/duration@npm:^1.1.0": - version: 1.1.0 - resolution: "@sapphire/duration@npm:1.1.0" - checksum: 10/78e734594f91f6cbf30888d86d4666de3279971246ef8b261523b465e50331cd05fe09f4e2b46e6bf8397d270349b8b68fd316a7aed5108cfba02a01299e6ff7 +"@sapphire/snowflake@npm:^3.2.2": + version: 3.2.2 + resolution: "@sapphire/snowflake@npm:3.2.2" + checksum: 10/3b88706136e6a1ddcbbdd6b64d161515ef017d0f892bc580d4f9955e09c5edca14eda7594a0dfe8ba78d50273d9a6a02668d586c6b53ceb125f58f91e008a7bb languageName: node linkType: hard -"@sapphire/framework@npm:^4.2.1": - version: 4.4.4 - resolution: "@sapphire/framework@npm:4.4.4" - dependencies: - "@discordjs/builders": "npm:^1.6.3" - "@sapphire/discord-utilities": "npm:^3.0.3" - "@sapphire/discord.js-utilities": "npm:^6.1.0" - "@sapphire/lexure": "npm:^1.1.5" - "@sapphire/pieces": "npm:^3.6.3" - "@sapphire/ratelimits": "npm:^2.4.6" - "@sapphire/result": "npm:^2.6.4" - "@sapphire/stopwatch": "npm:^1.5.0" - "@sapphire/utilities": "npm:^3.11.2" - checksum: 10/b725fa1091b17d5674f29b16ee5a08a5e25849a870d73a94c7b9b395c680ba17a889f78f81e77fd40bca5e66750d319fd31cbaf713527ac9e9e88c4a575ba453 +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: 10/297f95ff77c82c54de8c9907f186076e715ff2621c5222ba50b8d40a170661c0c5242c763cba2a4791f0f91cb1d8ffa53ea1d7294570cf8cd4694c0e383e484d languageName: node linkType: hard -"@sapphire/lexure@npm:^1.1.5": - version: 1.1.5 - resolution: "@sapphire/lexure@npm:1.1.5" - dependencies: - "@sapphire/result": "npm:^2.6.4" - checksum: 10/898d31fb54fa8106e8ee97f59f0866f0b421a6a055fb09622cd569312303dffaca689ec862fbacfd6848a4b65798a8cdbd9b4332b4107e44b39f8800bb4e87b6 +"@sindresorhus/is@npm:^5.2.0": + version: 5.6.0 + resolution: "@sindresorhus/is@npm:5.6.0" + checksum: 10/b077c325acec98e30f7d86df158aaba2e7af2acb9bb6a00fda4b91578539fbff4ecebe9b934e24fec0e6950de3089d89d79ec02d9062476b20ce185be0e01bd6 languageName: node linkType: hard -"@sapphire/pieces@npm:^3.6.3": - version: 3.6.3 - resolution: "@sapphire/pieces@npm:3.6.3" +"@sindresorhus/slugify@npm:^2.1.1": + version: 2.2.1 + resolution: "@sindresorhus/slugify@npm:2.2.1" dependencies: - "@discordjs/collection": "npm:^1.5.1" - "@sapphire/utilities": "npm:^3.11.1" - tslib: "npm:^2.5.0" - checksum: 10/17e3932ef1a5c8860700633819a6c80dfe0035e07f35c36f45aae0d536bd19a7f82f7764a683ab3889e4a5c2441f155c19ac8e9e79a065b030933a9e74ac415d + "@sindresorhus/transliterate": "npm:^1.0.0" + escape-string-regexp: "npm:^5.0.0" + checksum: 10/717f04cf71261ebac1284b982bd090cd4a29c277c146a42f861622dfd092504c65b20375ee41c6be3db30c483731bdf0377f3cdad9e8e9dc3f69560049aa203c languageName: node linkType: hard -"@sapphire/plugin-api@npm:^5.0.1": - version: 5.1.0 - resolution: "@sapphire/plugin-api@npm:5.1.0" +"@sindresorhus/transliterate@npm:^1.0.0": + version: 1.6.0 + resolution: "@sindresorhus/transliterate@npm:1.6.0" dependencies: - "@types/ws": "npm:^8.5.5" - tldts: "npm:^6.0.5" - tslib: "npm:^2.5.3" - undici: "npm:^5.22.1" - checksum: 10/4d7cb1a9a5dae5e0aa691009150cc3fabf9df6eb627d12c7ac087d915cac29d7f3d19d6e2d5a52b31b442eedd3ae499d864703b78f7747ca94c6fc9ee4b41892 - languageName: node - linkType: hard - -"@sapphire/plugin-hmr@npm:^2.0.0": - version: 2.0.1 - resolution: "@sapphire/plugin-hmr@npm:2.0.1" - dependencies: - chokidar: "npm:^3.5.3" - tslib: "npm:2.x" - checksum: 10/c8eb4062878387b680487a5fc7b3d99fc8213220f19877c69abfcbea5ef45377edfe7c0fa46b132d50bbb97f71c35048a3c0c172b99ea52de05e280c0f3443ec - languageName: node - linkType: hard - -"@sapphire/plugin-logger@npm:^3.0.1": - version: 3.0.4 - resolution: "@sapphire/plugin-logger@npm:3.0.4" - dependencies: - "@sapphire/timestamp": "npm:^1.0.1" - colorette: "npm:^2.0.20" - tslib: "npm:^2.5.0" - checksum: 10/574378879c82a2bc96dc28394ba7861e5097974ba19207907cb48830d06904f996d9af4f5dbc813a747d76ef1e920dbef625ce457e2c8dab5d859480eda107ce - languageName: node - linkType: hard - -"@sapphire/prettier-config@npm:^1.4.5": - version: 1.4.5 - resolution: "@sapphire/prettier-config@npm:1.4.5" - dependencies: - prettier: "npm:^2.8.2" - checksum: 10/378ce2099543a537c4f44b0a75f75ffc37e5802d0dd088bee8f3be5a2cb9616ac91ee952594b541624cf32648966d4b023d3e17a4b2cac66ee1a17aba92d8b86 - languageName: node - linkType: hard - -"@sapphire/ratelimits@npm:^2.4.6": - version: 2.4.6 - resolution: "@sapphire/ratelimits@npm:2.4.6" - checksum: 10/a83223893e4462f3e209679a206d7e0dd7f46771fc57522c1c5683c053977c314fa859eadd63317a832bdd909af583ce21007d4b2aae76b33aa78224f606679c - languageName: node - linkType: hard - -"@sapphire/result@npm:^2.6.0": - version: 2.6.0 - resolution: "@sapphire/result@npm:2.6.0" - checksum: 10/548cb7cf63f6d17a0937e2575d85104bb607e529d5b97bc71dabb5896094432e712a1f3e28e374565f069ccb123bd6799a42810af65daa53f02452000e184c65 - languageName: node - linkType: hard - -"@sapphire/result@npm:^2.6.4": - version: 2.6.4 - resolution: "@sapphire/result@npm:2.6.4" - checksum: 10/d47807b9f3ccf572b208d1052aafbc9f5f37f2d47bc661225d2dff0eb873c684bc37bcb5f0885a19309da37263be92743f611a140ba8601261ae2ae668c21a22 - languageName: node - linkType: hard - -"@sapphire/shapeshift@npm:^3.5.1": - version: 3.5.1 - resolution: "@sapphire/shapeshift@npm:3.5.1" - dependencies: - fast-deep-equal: "npm:^3.1.3" - lodash.uniqwith: "npm:^4.5.0" - checksum: 10/3791c9b17507eb864656520fcaba17dc660e39f3b2e08d2560438b60ba7752dbdc502ca6db378498f2961d5fe12ca6fae0b4c6e41cff06896d940525981d775b - languageName: node - linkType: hard - -"@sapphire/shapeshift@npm:^3.8.2": - version: 3.9.2 - resolution: "@sapphire/shapeshift@npm:3.9.2" - dependencies: - fast-deep-equal: "npm:^3.1.3" - lodash: "npm:^4.17.21" - checksum: 10/1f268ada96f1716ade47dac5fd5449e29fb2a04a2080f7bba0ea8d94f8fc83b5608e7f9fdbbfd28c0b3921997a8d60c875712f1cf4e14ea96e95b5f72af4d9ea - languageName: node - linkType: hard - -"@sapphire/snowflake@npm:^3.2.2": - version: 3.2.2 - resolution: "@sapphire/snowflake@npm:3.2.2" - checksum: 10/3b88706136e6a1ddcbbdd6b64d161515ef017d0f892bc580d4f9955e09c5edca14eda7594a0dfe8ba78d50273d9a6a02668d586c6b53ceb125f58f91e008a7bb - languageName: node - linkType: hard - -"@sapphire/stopwatch@npm:^1.5.0": - version: 1.5.0 - resolution: "@sapphire/stopwatch@npm:1.5.0" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10/86e50cb851ea744922cf95341e312261f9c5b1e866a09bc2ef1186dbb5db50bc3bc15baa86bab1773777035c4caa22c2c3c176611039864d3e9ff290c6f2d513 - languageName: node - linkType: hard - -"@sapphire/timestamp@npm:^1.0.1": - version: 1.0.1 - resolution: "@sapphire/timestamp@npm:1.0.1" - checksum: 10/6bed5ac6efb82df3ae14e4215395b43b2c7ea1afb7b0f0da08e43c779ad851e2557f6b02dbd05a567e3134a5f6aef5bd71fce5cde3153299e4cf21dd0a4074a0 - languageName: node - linkType: hard - -"@sapphire/ts-config@npm:^3.3.4": - version: 3.3.4 - resolution: "@sapphire/ts-config@npm:3.3.4" - dependencies: - tslib: "npm:^2.3.1" - typescript: "npm:^4.6.3" - checksum: 10/8a163806edeac8f501ebeec3cdb025a1095af46ba75ce25f4938ec0f1b6e07cd56e91e359cff7c56a2a52fb1a25e4c672946d95bb5e729ae65ddd7e6da895c3e - languageName: node - linkType: hard - -"@sapphire/utilities@npm:^3.11.1, @sapphire/utilities@npm:^3.11.2": - version: 3.12.0 - resolution: "@sapphire/utilities@npm:3.12.0" - checksum: 10/403cc1afd7d17f28b8857284a34176dff4acef46aa3b0e0b0939a650fd5fe593986e96e2d49b978b3f02b1b07dadc8d28ad1b2833387f33689952fdf7076ffce - languageName: node - linkType: hard - -"@sinclair/typebox@npm:^0.27.8": - version: 0.27.8 - resolution: "@sinclair/typebox@npm:0.27.8" - checksum: 10/297f95ff77c82c54de8c9907f186076e715ff2621c5222ba50b8d40a170661c0c5242c763cba2a4791f0f91cb1d8ffa53ea1d7294570cf8cd4694c0e383e484d - languageName: node - linkType: hard - -"@sindresorhus/is@npm:^4.0.0": - version: 4.6.0 - resolution: "@sindresorhus/is@npm:4.6.0" - checksum: 10/e7f36ed72abfcd5e0355f7423a72918b9748bb1ef370a59f3e5ad8d40b728b85d63b272f65f63eec1faf417cda89dcb0aeebe94015647b6054659c1442fe5ce0 - languageName: node - linkType: hard - -"@sindresorhus/is@npm:^5.2.0": - version: 5.6.0 - resolution: "@sindresorhus/is@npm:5.6.0" - checksum: 10/b077c325acec98e30f7d86df158aaba2e7af2acb9bb6a00fda4b91578539fbff4ecebe9b934e24fec0e6950de3089d89d79ec02d9062476b20ce185be0e01bd6 - languageName: node - linkType: hard - -"@sindresorhus/slugify@npm:^2.1.1": - version: 2.2.1 - resolution: "@sindresorhus/slugify@npm:2.2.1" - dependencies: - "@sindresorhus/transliterate": "npm:^1.0.0" - escape-string-regexp: "npm:^5.0.0" - checksum: 10/717f04cf71261ebac1284b982bd090cd4a29c277c146a42f861622dfd092504c65b20375ee41c6be3db30c483731bdf0377f3cdad9e8e9dc3f69560049aa203c - languageName: node - linkType: hard - -"@sindresorhus/transliterate@npm:^1.0.0": - version: 1.6.0 - resolution: "@sindresorhus/transliterate@npm:1.6.0" - dependencies: - escape-string-regexp: "npm:^5.0.0" - checksum: 10/fbb5bbcaf986068dc5aec87ef18380f46a8beaf0c5a7a5adf6cee26ceacde564b21381b1068d0beae86e489c2ef368ca15042a86a196762f59feca25db66abb3 - languageName: node - linkType: hard - -"@skyra/env-utilities@npm:^1.1.0": - version: 1.2.1 - resolution: "@skyra/env-utilities@npm:1.2.1" - dependencies: - dotenv: "npm:^16.3.0" - dotenv-expand: "npm:^10.0.0" - checksum: 10/e9ba9f38197ae1b5b339c5f126113ff0e41475f34faa52ed2e0472031b6d3f540e04e56a54858eafdc7bf8cc7b84f0a925ebaa55fe668884d65823ea72abcac5 + escape-string-regexp: "npm:^5.0.0" + checksum: 10/fbb5bbcaf986068dc5aec87ef18380f46a8beaf0c5a7a5adf6cee26ceacde564b21381b1068d0beae86e489c2ef368ca15042a86a196762f59feca25db66abb3 languageName: node linkType: hard @@ -3539,144 +2969,6 @@ __metadata: languageName: node linkType: hard -"@swc/cli@npm:^0.1.62": - version: 0.1.62 - resolution: "@swc/cli@npm:0.1.62" - dependencies: - "@mole-inc/bin-wrapper": "npm:^8.0.1" - commander: "npm:^7.1.0" - fast-glob: "npm:^3.2.5" - semver: "npm:^7.3.8" - slash: "npm:3.0.0" - source-map: "npm:^0.7.3" - peerDependencies: - "@swc/core": ^1.2.66 - chokidar: ^3.5.1 - peerDependenciesMeta: - chokidar: - optional: true - bin: - spack: bin/spack.js - swc: bin/swc.js - swcx: bin/swcx.js - checksum: 10/be1a44f8e9831b52ff92cfe7b2d8034fde64d800751e3698c5ae815eb7dfe27a58770fc79ac677195250ecf14dd58d18ac02fb51752533d666539d389e4ff864 - languageName: node - linkType: hard - -"@swc/core-darwin-arm64@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-darwin-arm64@npm:1.3.65" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@swc/core-darwin-x64@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-darwin-x64@npm:1.3.65" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@swc/core-linux-arm-gnueabihf@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.65" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@swc/core-linux-arm64-gnu@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.65" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@swc/core-linux-arm64-musl@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.65" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@swc/core-linux-x64-gnu@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.65" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@swc/core-linux-x64-musl@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-linux-x64-musl@npm:1.3.65" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@swc/core-win32-arm64-msvc@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.65" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@swc/core-win32-ia32-msvc@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.65" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@swc/core-win32-x64-msvc@npm:1.3.65": - version: 1.3.65 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.65" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@swc/core@npm:^1.3.37": - version: 1.3.65 - resolution: "@swc/core@npm:1.3.65" - dependencies: - "@swc/core-darwin-arm64": "npm:1.3.65" - "@swc/core-darwin-x64": "npm:1.3.65" - "@swc/core-linux-arm-gnueabihf": "npm:1.3.65" - "@swc/core-linux-arm64-gnu": "npm:1.3.65" - "@swc/core-linux-arm64-musl": "npm:1.3.65" - "@swc/core-linux-x64-gnu": "npm:1.3.65" - "@swc/core-linux-x64-musl": "npm:1.3.65" - "@swc/core-win32-arm64-msvc": "npm:1.3.65" - "@swc/core-win32-ia32-msvc": "npm:1.3.65" - "@swc/core-win32-x64-msvc": "npm:1.3.65" - peerDependencies: - "@swc/helpers": ^0.5.0 - dependenciesMeta: - "@swc/core-darwin-arm64": - optional: true - "@swc/core-darwin-x64": - optional: true - "@swc/core-linux-arm-gnueabihf": - optional: true - "@swc/core-linux-arm64-gnu": - optional: true - "@swc/core-linux-arm64-musl": - optional: true - "@swc/core-linux-x64-gnu": - optional: true - "@swc/core-linux-x64-musl": - optional: true - "@swc/core-win32-arm64-msvc": - optional: true - "@swc/core-win32-ia32-msvc": - optional: true - "@swc/core-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@swc/helpers": - optional: true - checksum: 10/6e7aff1c8716804abace0ff7fc5637085b5918eefac7c78a4cebf77d1a2b05467068221f2bc5a1d5f541dcd2f59ef2b3442fc54cbc061d6f63257c947eb1c2cd - languageName: node - linkType: hard - "@swc/helpers@npm:0.5.1": version: 0.5.1 resolution: "@swc/helpers@npm:0.5.1" @@ -3686,15 +2978,6 @@ __metadata: languageName: node linkType: hard -"@szmarczak/http-timer@npm:^4.0.5": - version: 4.0.6 - resolution: "@szmarczak/http-timer@npm:4.0.6" - dependencies: - defer-to-connect: "npm:^2.0.0" - checksum: 10/c29df3bcec6fc3bdec2b17981d89d9c9fc9bd7d0c9bcfe92821dc533f4440bc890ccde79971838b4ceed1921d456973c4180d7175ee1d0023ad0562240a58d95 - languageName: node - linkType: hard - "@szmarczak/http-timer@npm:^5.0.1": version: 5.0.1 resolution: "@szmarczak/http-timer@npm:5.0.1" @@ -3746,18 +3029,6 @@ __metadata: languageName: node linkType: hard -"@types/cacheable-request@npm:^6.0.1": - version: 6.0.3 - resolution: "@types/cacheable-request@npm:6.0.3" - dependencies: - "@types/http-cache-semantics": "npm:*" - "@types/keyv": "npm:^3.1.4" - "@types/node": "npm:*" - "@types/responselike": "npm:^1.0.0" - checksum: 10/159f9fdb2a1b7175eef453ae2ced5ea04c0d2b9610cc9ccd9f9abb066d36dacb1f37acd879ace10ad7cbb649490723feb396fb7307004c9670be29636304b988 - languageName: node - linkType: hard - "@types/chai-subset@npm:^1.3.3": version: 1.3.3 resolution: "@types/chai-subset@npm:1.3.3" @@ -3840,13 +3111,6 @@ __metadata: languageName: node linkType: hard -"@types/http-cache-semantics@npm:*": - version: 4.0.1 - resolution: "@types/http-cache-semantics@npm:4.0.1" - checksum: 10/d059bf8a15d5163cc60da51ba00d17620507f968d0b792cd55f62043016344a5f0e1aa94fa411089d41114035fcd0ea656f968bda7eabb6663a97787e3445a1c - languageName: node - linkType: hard - "@types/http-cache-semantics@npm:^4.0.2": version: 4.0.4 resolution: "@types/http-cache-semantics@npm:4.0.4" @@ -3898,15 +3162,6 @@ __metadata: languageName: node linkType: hard -"@types/keyv@npm:^3.1.4": - version: 3.1.4 - resolution: "@types/keyv@npm:3.1.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/e009a2bfb50e90ca9b7c6e8f648f8464067271fd99116f881073fa6fa76dc8d0133181dd65e6614d5fb1220d671d67b0124aef7d97dc02d7e342ab143a47779d - languageName: node - linkType: hard - "@types/mdast@npm:^3.0.0": version: 3.0.11 resolution: "@types/mdast@npm:3.0.11" @@ -3962,20 +3217,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.11.18": - version: 18.11.18 - resolution: "@types/node@npm:18.11.18" - checksum: 10/da05cf3a0036ef05cd695ac4cb265948593acbe723ba818f0ca0ce466b13ba99e1aac3a363086d6b8c7ea8f30c9233478e0293ac878a6f4b1d5515b10c392257 - languageName: node - linkType: hard - -"@types/node@npm:^18.14.6": - version: 18.16.18 - resolution: "@types/node@npm:18.16.18" - checksum: 10/4692b4c927bf63efbc3aed6e18c1a264eb8b5673fa7cc0649a38f394ab34d4d4d0e7a2b98f4235bc8dc7615e88080a1b443a06eac9324f67e426398ed857b4a1 - languageName: node - linkType: hard - "@types/node@npm:^18.6.3": version: 18.6.3 resolution: "@types/node@npm:18.6.3" @@ -4042,15 +3283,6 @@ __metadata: languageName: node linkType: hard -"@types/responselike@npm:^1.0.0": - version: 1.0.0 - resolution: "@types/responselike@npm:1.0.0" - dependencies: - "@types/node": "npm:*" - checksum: 10/e4972389457e4edce3cbba5e8474fb33684d73879433a9eec989d0afb7e550fd6fa3ffb8fe68dbb429288d10707796a193bc0007c4e8429fd267bdc4d8404632 - languageName: node - linkType: hard - "@types/scheduler@npm:*": version: 0.16.2 resolution: "@types/scheduler@npm:0.16.2" @@ -4095,15 +3327,6 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.4": - version: 8.5.4 - resolution: "@types/ws@npm:8.5.4" - dependencies: - "@types/node": "npm:*" - checksum: 10/8ad37f8ec1f7a1e2b8c0d53353ac30d182277c0bce4d877a497a0b7bcfbeee1838270eb6247a6978da66cc2891106d3c77511ebc827c58967ede8e756446422f - languageName: node - linkType: hard - "@types/ws@npm:^8.5.5": version: 8.5.5 resolution: "@types/ws@npm:8.5.5" @@ -4357,7 +3580,7 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1, abbrev@npm:^1.0.0": +"abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 @@ -4451,15 +3674,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" - dependencies: - debug: "npm:^4.3.4" - checksum: 10/f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f - languageName: node - linkType: hard - "agentkeepalive@npm:^4.2.1": version: 4.2.1 resolution: "agentkeepalive@npm:4.2.1" @@ -4555,15 +3769,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.1": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: "npm:^1.9.0" - checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 - languageName: node - linkType: hard - "ansi-styles@npm:^4.0.0, ansi-styles@npm:^4.1.0": version: 4.3.0 resolution: "ansi-styles@npm:4.3.0" @@ -4604,23 +3809,6 @@ __metadata: languageName: node linkType: hard -"arch@npm:^2.1.0": - version: 2.2.0 - resolution: "arch@npm:2.2.0" - checksum: 10/e35dbc6d362297000ab90930069576ba165fe63cd52383efcce14bd66c1b16a91ce849e1fd239964ed029d5e0bdfc32f68e9c7331b7df6c84ddebebfdbf242f7 - languageName: node - linkType: hard - -"are-we-there-yet@npm:^2.0.0": - version: 2.0.0 - resolution: "are-we-there-yet@npm:2.0.0" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^3.6.0" - checksum: 10/ea6f47d14fc33ae9cbea3e686eeca021d9d7b9db83a306010dd04ad5f2c8b7675291b127d3fcbfcbd8fec26e47b3324ad5b469a6cc3733a582f2fe4e12fc6756 - languageName: node - linkType: hard - "are-we-there-yet@npm:^3.0.0": version: 3.0.1 resolution: "are-we-there-yet@npm:3.0.1" @@ -4826,17 +4014,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.5.0": - version: 1.5.0 - resolution: "axios@npm:1.5.0" - dependencies: - follow-redirects: "npm:^1.15.0" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10/128433020b1fe9a460121735016f377adc6109a1f62b61795b1a80704de0a70affb0d580c8abd057e28af5f343cb4fb9a17a0b2512ea7f314578bbe492851a23 - languageName: node - linkType: hard - "axobject-query@npm:^3.1.1": version: 3.2.1 resolution: "axobject-query@npm:3.2.1" @@ -4888,37 +4065,6 @@ __metadata: languageName: node linkType: hard -"bin-check@npm:^4.1.0": - version: 4.1.0 - resolution: "bin-check@npm:4.1.0" - dependencies: - execa: "npm:^0.7.0" - executable: "npm:^4.1.0" - checksum: 10/16f6d5d86df9365dab682c7dd238f93678b773a908b3bccea4b1acb82b9b4e49fcfa24c99b99180a8e4cdd89a8f15f03700b09908ed5ae651f52fd82488a3507 - languageName: node - linkType: hard - -"bin-version-check@npm:^5.0.0": - version: 5.0.0 - resolution: "bin-version-check@npm:5.0.0" - dependencies: - bin-version: "npm:^6.0.0" - semver: "npm:^7.3.5" - semver-truncate: "npm:^2.0.0" - checksum: 10/1d3dc92847f8ecd5e07109f5f44727f0cb3b17c00be5ae2a2e105b86bf161bc4e5c10ee2e2c21d5d28e6382994d8416b5e06048191a485be909a1e49a959c3c3 - languageName: node - linkType: hard - -"bin-version@npm:^6.0.0": - version: 6.0.0 - resolution: "bin-version@npm:6.0.0" - dependencies: - execa: "npm:^5.0.0" - find-versions: "npm:^5.0.0" - checksum: 10/78c29422ea9597eb4c8d4f0eff96df60d09aa82b53a87925bc403efbe5c55251b1a07baac538381d9096377f92d27e3c03963efa86db5bc0d6431b9563946229 - languageName: node - linkType: hard - "binary-extensions@npm:^2.0.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -4957,13 +4103,6 @@ __metadata: languageName: node linkType: hard -"boolbase@npm:^1.0.0": - version: 1.0.0 - resolution: "boolbase@npm:1.0.0" - checksum: 10/3e25c80ef626c3a3487c73dbfc70ac322ec830666c9ad915d11b701142fab25ec1e63eff2c450c74347acfd2de854ccde865cd79ef4db1683f7c7b046ea43bb0 - languageName: node - linkType: hard - "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -5015,13 +4154,6 @@ __metadata: languageName: node linkType: hard -"buffer-from@npm:^1.0.0": - version: 1.1.2 - resolution: "buffer-from@npm:1.1.2" - checksum: 10/0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb - languageName: node - linkType: hard - "buffer@npm:^6.0.3": version: 6.0.3 resolution: "buffer@npm:6.0.3" @@ -5052,7 +4184,7 @@ __metadata: languageName: node linkType: hard -"busboy@npm:1.6.0, busboy@npm:^1.6.0": +"busboy@npm:1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" dependencies: @@ -5101,13 +4233,6 @@ __metadata: languageName: node linkType: hard -"cacheable-lookup@npm:^5.0.3": - version: 5.0.4 - resolution: "cacheable-lookup@npm:5.0.4" - checksum: 10/618a8b3eea314060e74cb3285a6154e8343c244a34235acf91cfe626ee0705c24e3cd11e4b1a7b3900bd749ee203ae65afe13adf610c8ab173e99d4a208faf75 - languageName: node - linkType: hard - "cacheable-lookup@npm:^7.0.0": version: 7.0.0 resolution: "cacheable-lookup@npm:7.0.0" @@ -5130,21 +4255,6 @@ __metadata: languageName: node linkType: hard -"cacheable-request@npm:^7.0.2": - version: 7.0.4 - resolution: "cacheable-request@npm:7.0.4" - dependencies: - clone-response: "npm:^1.0.2" - get-stream: "npm:^5.1.0" - http-cache-semantics: "npm:^4.0.0" - keyv: "npm:^4.0.0" - lowercase-keys: "npm:^2.0.0" - normalize-url: "npm:^6.0.1" - responselike: "npm:^2.0.0" - checksum: 10/0f4f2001260ecca78b9f64fc8245e6b5a5dcde24ea53006daab71f5e0e1338095aa1512ec099c4f9895a9e5acfac9da423cb7c079e131485891e9214aca46c41 - languageName: node - linkType: hard - "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": version: 1.0.2 resolution: "call-bind@npm:1.0.2" @@ -5212,17 +4322,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.4.1": - version: 2.4.2 - resolution: "chalk@npm:2.4.2" - dependencies: - ansi-styles: "npm:^3.2.1" - escape-string-regexp: "npm:^1.0.5" - supports-color: "npm:^5.3.0" - checksum: 10/3d1d103433166f6bfe82ac75724951b33769675252d8417317363ef9d54699b7c3b2d46671b772b893a8e50c3ece70c4b933c73c01e81bc60ea4df9b55afa303 - languageName: node - linkType: hard - "chalk@npm:^4.0.0": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -5365,15 +4464,6 @@ __metadata: languageName: node linkType: hard -"clone-response@npm:^1.0.2": - version: 1.0.3 - resolution: "clone-response@npm:1.0.3" - dependencies: - mimic-response: "npm:^1.0.0" - checksum: 10/4e671cac39b11c60aa8ba0a450657194a5d6504df51bca3fac5b3bd0145c4f8e8464898f87c8406b83232e3bc5cca555f51c1f9c8ac023969ebfbf7f6bdabb2e - languageName: node - linkType: hard - "clone@npm:^1.0.2": version: 1.0.4 resolution: "clone@npm:1.0.4" @@ -5401,15 +4491,6 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0": - version: 1.9.3 - resolution: "color-convert@npm:1.9.3" - dependencies: - color-name: "npm:1.1.3" - checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10 - languageName: node - linkType: hard - "color-convert@npm:^2.0.1": version: 2.0.1 resolution: "color-convert@npm:2.0.1" @@ -5419,13 +4500,6 @@ __metadata: languageName: node linkType: hard -"color-name@npm:1.1.3": - version: 1.1.3 - resolution: "color-name@npm:1.1.3" - checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d - languageName: node - linkType: hard - "color-name@npm:^1.0.0, color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" @@ -5443,7 +4517,7 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.2, color-support@npm:^1.1.3": +"color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" bin: @@ -5462,13 +4536,6 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.19, colorette@npm:^2.0.20": - version: 2.0.20 - resolution: "colorette@npm:2.0.20" - checksum: 10/0b8de48bfa5d10afc160b8eaa2b9938f34a892530b2f7d7897e0458d9535a066e3998b49da9d21161c78225b272df19ae3a64d6df28b4c9734c0e55bbd02406f - languageName: node - linkType: hard - "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -5499,13 +4566,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^7.1.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 10/9973af10727ad4b44f26703bf3e9fdc323528660a7590efe3aa9ad5042b4584c0deed84ba443f61c9d6f02dade54a5a5d3c95e306a1e1630f8374ae6db16c06d - languageName: node - linkType: hard - "commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -5527,14 +4587,14 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": +"console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" checksum: 10/27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb languageName: node linkType: hard -"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.4": +"content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -5581,30 +4641,6 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^5.0.1": - version: 5.1.0 - resolution: "cross-spawn@npm:5.1.0" - dependencies: - lru-cache: "npm:^4.0.1" - shebang-command: "npm:^1.2.0" - which: "npm:^1.2.9" - checksum: 10/726939c9954fc70c20e538923feaaa33bebc253247d13021737c3c7f68cdc3e0a57f720c0fe75057c0387995349f3f12e20e9bfdbf12274db28019c7ea4ec166 - languageName: node - linkType: hard - -"cross-spawn@npm:^6.0.5": - version: 6.0.5 - resolution: "cross-spawn@npm:6.0.5" - dependencies: - nice-try: "npm:^1.0.4" - path-key: "npm:^2.0.1" - semver: "npm:^5.5.0" - shebang-command: "npm:^1.2.0" - which: "npm:^1.2.9" - checksum: 10/f07e643b4875f26adffcd7f13bc68d9dff20cf395f8ed6f43a23f3ee24fc3a80a870a32b246fd074e514c8fd7da5f978ac6a7668346eec57aa87bac89c1ed3a1 - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -5616,26 +4652,6 @@ __metadata: languageName: node linkType: hard -"css-select@npm:^5.1.0": - version: 5.1.0 - resolution: "css-select@npm:5.1.0" - dependencies: - boolbase: "npm:^1.0.0" - css-what: "npm:^6.1.0" - domhandler: "npm:^5.0.2" - domutils: "npm:^3.0.1" - nth-check: "npm:^2.0.1" - checksum: 10/d486b1e7eb140468218a5ab5af53257e01f937d2173ac46981f6b7de9c5283d55427a36715dc8decfc0c079cf89259ac5b41ef58f6e1a422eee44ab8bfdc78da - languageName: node - linkType: hard - -"css-what@npm:^6.1.0": - version: 6.1.0 - resolution: "css-what@npm:6.1.0" - checksum: 10/c67a3a2d0d81843af87f8bf0a4d0845b0f952377714abbb2884e48942409d57a2110eabee003609d02ee487b054614bdfcfc59ee265728ff105bd5aa221c1d0e - languageName: node - linkType: hard - "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -5659,20 +4675,6 @@ __metadata: languageName: node linkType: hard -"dargs@npm:~7.0.0": - version: 7.0.0 - resolution: "dargs@npm:7.0.0" - checksum: 10/b8f1e3cba59c42e1f13a114ad4848c3fc1cf7470f633ee9e9f1043762429bc97d91ae31b826fb135eefde203a3fdb20deb0c0a0222ac29d937b8046085d668d1 - languageName: node - linkType: hard - -"data-uri-to-buffer@npm:^4.0.0": - version: 4.0.1 - resolution: "data-uri-to-buffer@npm:4.0.1" - checksum: 10/0d0790b67ffec5302f204c2ccca4494f70b4e2d940fea3d36b09f0bb2b8539c2e86690429eb1f1dc4bcc9e4df0644193073e63d9ee48ac9fce79ec1506e4aa4c - languageName: node - linkType: hard - "date-fns@npm:^2.29.3": version: 2.30.0 resolution: "date-fns@npm:2.30.0" @@ -5789,7 +4791,7 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": +"defer-to-connect@npm:^2.0.1": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" checksum: 10/8a9b50d2f25446c0bfefb55a48e90afd58f85b21bcf78e9207cd7b804354f6409032a1705c2491686e202e64fc05f147aa5aa45f9aa82627563f045937f5791b @@ -5869,13 +4871,6 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0": - version: 2.0.2 - resolution: "detect-libc@npm:2.0.2" - checksum: 10/6118f30c0c425b1e56b9d2609f29bec50d35a6af0b762b6ad127271478f3bbfda7319ce869230cf1a351f2b219f39332cde290858553336d652c77b970f15de8 - languageName: node - linkType: hard - "detect-libc@npm:^2.0.3": version: 2.0.3 resolution: "detect-libc@npm:2.0.3" @@ -5956,20 +4951,6 @@ __metadata: languageName: node linkType: hard -"discord-api-types@npm:^0.37.2": - version: 0.37.28 - resolution: "discord-api-types@npm:0.37.28" - checksum: 10/b478bc29b19234701e5c74edd8212a2673bf7aa0f1c37315e6149269099fd378fb61204cba67ce5289d6fc868929d21d3e34f98b9d3d9f3f563b363601b204aa - languageName: node - linkType: hard - -"discord-api-types@npm:^0.37.35, discord-api-types@npm:^0.37.41": - version: 0.37.46 - resolution: "discord-api-types@npm:0.37.46" - checksum: 10/3d3194a76ef59a18e82ce8dddf1eabe5285972dc3f5aceacdb80dbf1314c6f644c800b503a80bc49d98e2d3f4a51292af93f5f3aa7fc4ee5176eb1f25ffd7a4e - languageName: node - linkType: hard - "discord-api-types@npm:^0.37.50": version: 0.37.52 resolution: "discord-api-types@npm:0.37.52" @@ -5977,7 +4958,7 @@ __metadata: languageName: node linkType: hard -"discord-player@workspace:^, discord-player@workspace:packages/discord-player": +"discord-player@workspace:packages/discord-player": version: 0.0.0-use.local resolution: "discord-player@workspace:packages/discord-player" dependencies: @@ -5998,26 +4979,10 @@ __metadata: tsup: "npm:^7.2.0" typescript: "npm:^5.2.2" vitest: "npm:^0.34.6" - peerDependencies: - "@discord-player/extractor": "workspace:^" + ws: "npm:^8.17.0" languageName: unknown linkType: soft -"discord-voip@npm:^0.1.2": - version: 0.1.2 - resolution: "discord-voip@npm:0.1.2" - dependencies: - "@discord-player/ffmpeg": "npm:^0.1.0" - "@discord-player/opus": "npm:^0.1.0" - "@types/ws": "npm:^8.5.5" - discord-api-types: "npm:^0.37.50" - prism-media: "npm:^1.3.5" - tslib: "npm:^2.6.1" - ws: "npm:^8.13.0" - checksum: 10/5ffba1b2d9e914e58d68831606e1b319067a6d0c4195314fdd49575a87d2a2849757597ebb020965b54bb4be43acdd8c1ae7a50cc2f5ded8a8b38696ac1d5bb7 - languageName: node - linkType: hard - "discord-voip@npm:^0.1.3": version: 0.1.3 resolution: "discord-voip@npm:0.1.3" @@ -6103,65 +5068,6 @@ __metadata: languageName: node linkType: hard -"dom-serializer@npm:^2.0.0": - version: 2.0.0 - resolution: "dom-serializer@npm:2.0.0" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.2" - entities: "npm:^4.2.0" - checksum: 10/e3bf9027a64450bca0a72297ecdc1e3abb7a2912268a9f3f5d33a2e29c1e2c3502c6e9f860fc6625940bfe0cfb57a44953262b9e94df76872fdfb8151097eeb3 - languageName: node - linkType: hard - -"domelementtype@npm:^2.3.0": - version: 2.3.0 - resolution: "domelementtype@npm:2.3.0" - checksum: 10/ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 - languageName: node - linkType: hard - -"domhandler@npm:^5.0.1, domhandler@npm:^5.0.2": - version: 5.0.3 - resolution: "domhandler@npm:5.0.3" - dependencies: - domelementtype: "npm:^2.3.0" - checksum: 10/809b805a50a9c6884a29f38aec0a4e1b4537f40e1c861950ed47d10b049febe6b79ab72adaeeebb3cc8fc1cd33f34e97048a72a9265103426d93efafa78d3e96 - languageName: node - linkType: hard - -"domutils@npm:^3.0.1": - version: 3.0.1 - resolution: "domutils@npm:3.0.1" - dependencies: - dom-serializer: "npm:^2.0.0" - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.1" - checksum: 10/c0031e4bf89bf701c552c6aa7937262351ae863d5bb0395ebae9cdb23eb3de0077343ca0ddfa63861d98c31c02bbabe4c6e0e11be87b04a090a4d5dbb75197dc - languageName: node - linkType: hard - -"dotenv-expand@npm:^10.0.0": - version: 10.0.0 - resolution: "dotenv-expand@npm:10.0.0" - checksum: 10/b41eb278bc96b92cbf3037ca5f3d21e8845bf165dc06b6f9a0a03d278c2bd5a01c0cfbb3528ae3a60301ba1a8a9cace30e748c54b460753bc00d4c014b675597 - languageName: node - linkType: hard - -"dotenv@npm:^16.3.0": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" - checksum: 10/dbb778237ef8750e9e3cd1473d3c8eaa9cc3600e33a75c0e36415d0fa0848197f56c3800f77924c70e7828f0b03896818cd52f785b07b9ad4d88dba73fbba83f - languageName: node - linkType: hard - -"duplexer@npm:~0.1.1": - version: 0.1.2 - resolution: "duplexer@npm:0.1.2" - checksum: 10/62ba61a830c56801db28ff6305c7d289b6dc9f859054e8c982abd8ee0b0a14d2e9a8e7d086ffee12e868d43e2bbe8a964be55ddbd8c8957714c87373c7a4f9b0 - languageName: node - linkType: hard - "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -6206,15 +5112,6 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0": - version: 1.4.4 - resolution: "end-of-stream@npm:1.4.4" - dependencies: - once: "npm:^1.4.0" - checksum: 10/530a5a5a1e517e962854a31693dbb5c0b2fc40b46dad2a56a2deec656ca040631124f4795823acc68238147805f8b021abbe221f4afed5ef3c8e8efc2024908b - languageName: node - linkType: hard - "engine.io-parser@npm:~5.2.1": version: 5.2.2 resolution: "engine.io-parser@npm:5.2.2" @@ -6250,13 +5147,6 @@ __metadata: languageName: node linkType: hard -"entities@npm:^4.2.0": - version: 4.4.0 - resolution: "entities@npm:4.4.0" - checksum: 10/b627cb900e901cc7817037b83bf993a1cbf6a64850540f7526af7bcf9c7d09ebc671198e6182cfae4680f733799e2852e6a1c46aa62ff36eb99680057a038df5 - languageName: node - linkType: hard - "entities@npm:^4.4.0": version: 4.5.0 resolution: "entities@npm:4.5.0" @@ -6278,15 +5168,6 @@ __metadata: languageName: node linkType: hard -"error-ex@npm:^1.3.1": - version: 1.3.2 - resolution: "error-ex@npm:1.3.2" - dependencies: - is-arrayish: "npm:^0.2.1" - checksum: 10/d547740aa29c34e753fb6fed2c5de81802438529c12b3673bd37b6bb1fe49b9b7abdc3c11e6062fe625d8a296b3cf769a80f878865e25e685f787763eede3ffb - languageName: node - linkType: hard - "es-abstract@npm:^1.19.0, es-abstract@npm:^1.20.4": version: 1.21.2 resolution: "es-abstract@npm:1.21.2" @@ -6547,83 +5428,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:~0.17.6": - version: 0.17.19 - resolution: "esbuild@npm:0.17.19" - dependencies: - "@esbuild/android-arm": "npm:0.17.19" - "@esbuild/android-arm64": "npm:0.17.19" - "@esbuild/android-x64": "npm:0.17.19" - "@esbuild/darwin-arm64": "npm:0.17.19" - "@esbuild/darwin-x64": "npm:0.17.19" - "@esbuild/freebsd-arm64": "npm:0.17.19" - "@esbuild/freebsd-x64": "npm:0.17.19" - "@esbuild/linux-arm": "npm:0.17.19" - "@esbuild/linux-arm64": "npm:0.17.19" - "@esbuild/linux-ia32": "npm:0.17.19" - "@esbuild/linux-loong64": "npm:0.17.19" - "@esbuild/linux-mips64el": "npm:0.17.19" - "@esbuild/linux-ppc64": "npm:0.17.19" - "@esbuild/linux-riscv64": "npm:0.17.19" - "@esbuild/linux-s390x": "npm:0.17.19" - "@esbuild/linux-x64": "npm:0.17.19" - "@esbuild/netbsd-x64": "npm:0.17.19" - "@esbuild/openbsd-x64": "npm:0.17.19" - "@esbuild/sunos-x64": "npm:0.17.19" - "@esbuild/win32-arm64": "npm:0.17.19" - "@esbuild/win32-ia32": "npm:0.17.19" - "@esbuild/win32-x64": "npm:0.17.19" - dependenciesMeta: - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/86ada7cad6d37a3445858fee31ca39fc6c0436c7c00b2e07b9ce308235be67f36aefe0dda25da9ab08653fde496d1e759d6ad891ce9479f9e1fb4964c8f2a0fa - languageName: node - linkType: hard - "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -6645,13 +5449,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": - version: 1.0.5 - resolution: "escape-string-regexp@npm:1.0.5" - checksum: 10/6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 - languageName: node - linkType: hard - "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -7128,37 +5925,7 @@ __metadata: languageName: node linkType: hard -"event-stream@npm:=3.3.4": - version: 3.3.4 - resolution: "event-stream@npm:3.3.4" - dependencies: - duplexer: "npm:~0.1.1" - from: "npm:~0" - map-stream: "npm:~0.1.0" - pause-stream: "npm:0.0.11" - split: "npm:0.3" - stream-combiner: "npm:~0.0.4" - through: "npm:~2.3.1" - checksum: 10/48ea0e17df89ff45778c25e7111a6691401c902162823ddd7656d83fc972e75380f789f7a48f272f50fe7015420cc04f835d458560bf95e34b2c7a479570c8fb - languageName: node - linkType: hard - -"execa@npm:^0.7.0": - version: 0.7.0 - resolution: "execa@npm:0.7.0" - dependencies: - cross-spawn: "npm:^5.0.1" - get-stream: "npm:^3.0.0" - is-stream: "npm:^1.1.0" - npm-run-path: "npm:^2.0.0" - p-finally: "npm:^1.0.0" - signal-exit: "npm:^3.0.0" - strip-eof: "npm:^1.0.0" - checksum: 10/7c1721de38e51d67cef2367b1f6037c4a89bc64993d93683f9f740fc74d6930dfc92faf2749b917b7337a8d632d7b86a4673400f762bc1fe4a977ea6b0f9055f - languageName: node - linkType: hard - -"execa@npm:^5.0.0, execa@npm:~5.1.0": +"execa@npm:^5.0.0": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -7192,15 +5959,6 @@ __metadata: languageName: node linkType: hard -"executable@npm:^4.1.0": - version: 4.1.1 - resolution: "executable@npm:4.1.1" - dependencies: - pify: "npm:^2.2.0" - checksum: 10/f01927ce59bccec804e171bf859a26e362c1f50aa9ebc69f7cafdcce3859d29d4b6267fd47237c18b0a1830614bd3f0ee14b7380d9bad18a4e7af9b5f0b6984f - languageName: node - linkType: hard - "express@npm:^4.18.2": version: 4.19.2 resolution: "express@npm:4.19.2" @@ -7240,25 +5998,6 @@ __metadata: languageName: node linkType: hard -"ext-list@npm:^2.0.0": - version: 2.2.2 - resolution: "ext-list@npm:2.2.2" - dependencies: - mime-db: "npm:^1.28.0" - checksum: 10/fe69fedbef044e14d4ce9e84c6afceb696ba71500c15b8d0ce0a1e280237e17c95031b3d62d5e597652fea0065b9bf957346b3900d989dff59128222231ac859 - languageName: node - linkType: hard - -"ext-name@npm:^5.0.0": - version: 5.0.0 - resolution: "ext-name@npm:5.0.0" - dependencies: - ext-list: "npm:^2.0.0" - sort-keys-length: "npm:^1.0.0" - checksum: 10/f598269bd5de4295540ea7d6f8f6a01d82a7508f148b7700a05628ef6121648d26e6e5e942049e953b3051863df6b54bd8fe951e7877f185e34ace5d44370b33 - languageName: node - linkType: hard - "extend-shallow@npm:^2.0.1": version: 2.0.1 resolution: "extend-shallow@npm:2.0.1" @@ -7282,7 +6021,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.5": +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" dependencies: @@ -7351,16 +6090,6 @@ __metadata: languageName: node linkType: hard -"fetch-blob@npm:^3.1.2, fetch-blob@npm:^3.1.4": - version: 3.2.0 - resolution: "fetch-blob@npm:3.2.0" - dependencies: - node-domexception: "npm:^1.0.0" - web-streams-polyfill: "npm:^3.0.3" - checksum: 10/5264ecceb5fdc19eb51d1d0359921f12730941e333019e673e71eb73921146dceabcb0b8f534582be4497312d656508a439ad0f5edeec2b29ab2e10c72a1f86b - languageName: node - linkType: hard - "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -7370,18 +6099,7 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^16.5.4": - version: 16.5.4 - resolution: "file-type@npm:16.5.4" - dependencies: - readable-web-to-node-stream: "npm:^3.0.0" - strtok3: "npm:^6.2.4" - token-types: "npm:^4.1.1" - checksum: 10/46ced46bb925ab547e0a6d43108a26d043619d234cb0588d7abce7b578dafac142bcfd2e23a6adb0a4faa4b951bd1b14b355134a193362e07cd352f9bf0dc349 - languageName: node - linkType: hard - -"file-type@npm:^17.1.4, file-type@npm:^17.1.6": +"file-type@npm:^17.1.4": version: 17.1.6 resolution: "file-type@npm:17.1.6" dependencies: @@ -7392,24 +6110,6 @@ __metadata: languageName: node linkType: hard -"filename-reserved-regex@npm:^3.0.0": - version: 3.0.0 - resolution: "filename-reserved-regex@npm:3.0.0" - checksum: 10/1803e19ce64d7cb88ee5a1bd3ce282470a5c263987269222426d889049fc857e302284fa71937de9582eba7a9f39539557d45e0562f2fa51cade8efc68c65dd9 - languageName: node - linkType: hard - -"filenamify@npm:^5.0.2": - version: 5.1.1 - resolution: "filenamify@npm:5.1.1" - dependencies: - filename-reserved-regex: "npm:^3.0.0" - strip-outer: "npm:^2.0.0" - trim-repeated: "npm:^2.0.0" - checksum: 10/55a7ed0858eb2655bb1bb1e945a59e3fb30ba4767f6924fa064ccd731bff07678aac3cb4f3899ae0e1621fe81d6472b5688232bb6afd4eeb989ade785fc1c6f1 - languageName: node - linkType: hard - "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -7444,15 +6144,6 @@ __metadata: languageName: node linkType: hard -"find-versions@npm:^5.0.0": - version: 5.1.0 - resolution: "find-versions@npm:5.1.0" - dependencies: - semver-regex: "npm:^4.0.5" - checksum: 10/680bdb0081f631f7bfb6f0f8edcfa0b74ab8cabc82097a4527a37b0d042aabc56685bf459ff27991eab0baddc04eb8e3bba8a2869f5004ecf7cdd2779b6e51de - languageName: node - linkType: hard - "flat-cache@npm:^3.0.4": version: 3.0.4 resolution: "flat-cache@npm:3.0.4" @@ -7470,16 +6161,6 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.0": - version: 1.15.2 - resolution: "follow-redirects@npm:1.15.2" - peerDependenciesMeta: - debug: - optional: true - checksum: 10/8be0d39919770054812537d376850ccde0b4762b0501c440bd08724971a078123b55f57704f2984e0664fecc0c86adea85add63295804d9dce401cd9604c91d3 - languageName: node - linkType: hard - "follow-redirects@npm:^1.15.6": version: 1.15.6 resolution: "follow-redirects@npm:1.15.6" @@ -7524,15 +6205,6 @@ __metadata: languageName: node linkType: hard -"formdata-polyfill@npm:^4.0.10": - version: 4.0.10 - resolution: "formdata-polyfill@npm:4.0.10" - dependencies: - fetch-blob: "npm:^3.1.2" - checksum: 10/9b5001d2edef3c9449ac3f48bd4f8cc92e7d0f2e7c1a5c8ba555ad4e77535cc5cf621fabe49e97f304067037282dd9093b9160a3cb533e46420b446c4e6bc06f - languageName: node - linkType: hard - "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -7554,13 +6226,6 @@ __metadata: languageName: node linkType: hard -"from@npm:~0": - version: 0.1.7 - resolution: "from@npm:0.1.7" - checksum: 10/b85125b7890489656eb2e4f208f7654a93ec26e3aefaf3bbbcc0d496fc1941e4405834fcc9fe7333192aa2187905510ace70417bbf9ac6f6f4784a731d986939 - languageName: node - linkType: hard - "fs-extra@npm:^11.1.0": version: 11.1.0 resolution: "fs-extra@npm:11.1.0" @@ -7677,23 +6342,6 @@ __metadata: languageName: node linkType: hard -"gauge@npm:^3.0.0": - version: 3.0.2 - resolution: "gauge@npm:3.0.2" - dependencies: - aproba: "npm:^1.0.3 || ^2.0.0" - color-support: "npm:^1.1.2" - console-control-strings: "npm:^1.0.0" - has-unicode: "npm:^2.0.1" - object-assign: "npm:^4.1.1" - signal-exit: "npm:^3.0.0" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - wide-align: "npm:^1.1.2" - checksum: 10/46df086451672a5fecd58f7ec86da74542c795f8e00153fbef2884286ce0e86653c3eb23be2d0abb0c4a82b9b2a9dec3b09b6a1cf31c28085fa0376599a26589 - languageName: node - linkType: hard - "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -7717,16 +6365,6 @@ __metadata: languageName: node linkType: hard -"genius-lyrics@npm:^4.4.6": - version: 4.4.6 - resolution: "genius-lyrics@npm:4.4.6" - dependencies: - node-html-parser: "npm:^6.1.9" - undici: "npm:^5.24.0" - checksum: 10/468425609e21fb3268f319ee7ba18173c045e3a2d19fd5769bf42598c9532afc3e60568b0e1715a6d75f39430462bd000c0fa4cafcbe71face1187eb7f08edda - languageName: node - linkType: hard - "get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" @@ -7767,22 +6405,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "get-stream@npm:3.0.0" - checksum: 10/de14fbb3b4548ace9ab6376be852eef9898c491282e29595bc908a1814a126d3961b11cd4b7be5220019fe3b2abb84568da7793ad308fc139925a217063fa159 - languageName: node - linkType: hard - -"get-stream@npm:^5.1.0": - version: 5.2.0 - resolution: "get-stream@npm:5.2.0" - dependencies: - pump: "npm:^3.0.0" - checksum: 10/13a73148dca795e41421013da6e3ebff8ccb7fba4d2f023fd0c6da2c166ec4e789bec9774a73a7b49c08daf2cae552f8a3e914042ac23b5f59dd278cc8f9cbfb - languageName: node - linkType: hard - "get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -7800,7 +6422,7 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.4.0, get-tsconfig@npm:^4.5.0": +"get-tsconfig@npm:^4.5.0": version: 4.6.0 resolution: "get-tsconfig@npm:4.6.0" dependencies: @@ -7966,25 +6588,6 @@ __metadata: languageName: node linkType: hard -"got@npm:^11.8.5": - version: 11.8.6 - resolution: "got@npm:11.8.6" - dependencies: - "@sindresorhus/is": "npm:^4.0.0" - "@szmarczak/http-timer": "npm:^4.0.5" - "@types/cacheable-request": "npm:^6.0.1" - "@types/responselike": "npm:^1.0.0" - cacheable-lookup: "npm:^5.0.3" - cacheable-request: "npm:^7.0.2" - decompress-response: "npm:^6.0.0" - http2-wrapper: "npm:^1.0.0-beta.5.2" - lowercase-keys: "npm:^2.0.0" - p-cancelable: "npm:^2.0.0" - responselike: "npm:^2.0.0" - checksum: 10/a30c74029d81bd5fe50dea1a0c970595d792c568e188ff8be254b5bc11e6158d1b014570772d4a30d0a97723e7dd34e7c8cc1a2f23018f60aece3070a7a5c2a5 - languageName: node - linkType: hard - "got@npm:^12.0.0, got@npm:^12.1.0": version: 12.6.1 resolution: "got@npm:12.6.1" @@ -8070,13 +6673,6 @@ __metadata: languageName: node linkType: hard -"has-flag@npm:^3.0.0": - version: 3.0.0 - resolution: "has-flag@npm:3.0.0" - checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b - languageName: node - linkType: hard - "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -8413,29 +7009,6 @@ __metadata: languageName: node linkType: hard -"he@npm:1.2.0": - version: 1.2.0 - resolution: "he@npm:1.2.0" - bin: - he: bin/he - checksum: 10/d09b2243da4e23f53336e8de3093e5c43d2c39f8d0d18817abfa32ce3e9355391b2edb4bb5edc376aea5d4b0b59d6a0482aab4c52bc02ef95751e4b818e847f1 - languageName: node - linkType: hard - -"himalaya@npm:~1.1.0": - version: 1.1.0 - resolution: "himalaya@npm:1.1.0" - checksum: 10/cae7b3612951220a9cfe33e2df1a3eb0d12f9280be04637f4f60820fb46571248557923501802a803184c07de5ae425015a3406c3db6fdb50e406828fbbd0d4c - languageName: node - linkType: hard - -"hosted-git-info@npm:^2.1.4": - version: 2.8.9 - resolution: "hosted-git-info@npm:2.8.9" - checksum: 10/96da7d412303704af41c3819207a09ea2cab2de97951db4cf336bb8bce8d8e36b9a6821036ad2e55e67d3be0af8f967a7b57981203fbfb88bc05cd803407b8c3 - languageName: node - linkType: hard - "html-void-elements@npm:^2.0.0": version: 2.0.1 resolution: "html-void-elements@npm:2.0.1" @@ -8443,13 +7016,6 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": - version: 4.1.1 - resolution: "http-cache-semantics@npm:4.1.1" - checksum: 10/362d5ed66b12ceb9c0a328fb31200b590ab1b02f4a254a697dc796850cc4385603e75f53ec59f768b2dad3bfa1464bd229f7de278d2899a0e3beffc634b6683f - languageName: node - linkType: hard - "http-cache-semantics@npm:^4.1.0": version: 4.1.0 resolution: "http-cache-semantics@npm:4.1.0" @@ -8457,21 +7023,10 @@ __metadata: languageName: node linkType: hard -"http-cookie-agent@npm:^5.0.4": - version: 5.0.4 - resolution: "http-cookie-agent@npm:5.0.4" - dependencies: - agent-base: "npm:^7.1.0" - peerDependencies: - deasync: ^0.1.26 - tough-cookie: ^4.0.0 - undici: ^5.11.0 - peerDependenciesMeta: - deasync: - optional: true - undici: - optional: true - checksum: 10/c1cae8efb1353999c127b2590d2d012a4b09ddf00a87f06ca3f39c821b3d5afa409baaf1598ee34cb6b83e93013e1a84fa2c66e664296f83ede06a485cb0525e +"http-cache-semantics@npm:^4.1.1": + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 10/362d5ed66b12ceb9c0a328fb31200b590ab1b02f4a254a697dc796850cc4385603e75f53ec59f768b2dad3bfa1464bd229f7de278d2899a0e3beffc634b6683f languageName: node linkType: hard @@ -8499,16 +7054,6 @@ __metadata: languageName: node linkType: hard -"http2-wrapper@npm:^1.0.0-beta.5.2": - version: 1.0.3 - resolution: "http2-wrapper@npm:1.0.3" - dependencies: - quick-lru: "npm:^5.1.1" - resolve-alpn: "npm:^1.0.0" - checksum: 10/8097ee2699440c2e64bda52124990cc5b0fb347401c7797b1a0c1efd5a0f79a4ebaa68e8a6ac3e2dde5f09460c1602764da6da2412bad628ed0a3b0ae35e72d4 - languageName: node - linkType: hard - "http2-wrapper@npm:^2.1.10": version: 2.2.1 resolution: "http2-wrapper@npm:2.2.1" @@ -8736,13 +7281,6 @@ __metadata: languageName: node linkType: hard -"is-arrayish@npm:^0.2.1": - version: 0.2.1 - resolution: "is-arrayish@npm:0.2.1" - checksum: 10/73ced84fa35e59e2c57da2d01e12cd01479f381d7f122ce41dcbb713f09dbfc651315832cd2bf8accba7681a69e4d6f1e03941d94dd10040d415086360e7005e - languageName: node - linkType: hard - "is-arrayish@npm:^0.3.1": version: 0.3.2 resolution: "is-arrayish@npm:0.3.2" @@ -8957,13 +7495,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^1.0.0": - version: 1.1.0 - resolution: "is-plain-obj@npm:1.1.0" - checksum: 10/0ee04807797aad50859652a7467481816cbb57e5cc97d813a7dcd8915da8195dc68c436010bf39d195226cde6a2d352f4b815f16f26b7bf486a5754290629931 - languageName: node - linkType: hard - "is-plain-obj@npm:^4.0.0": version: 4.1.0 resolution: "is-plain-obj@npm:4.1.0" @@ -9006,13 +7537,6 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^1.1.0": - version: 1.1.0 - resolution: "is-stream@npm:1.1.0" - checksum: 10/351aa77c543323c4e111204482808cfad68d2e940515949e31ccd0b010fc13d5fba4b9c230e4887fd24284713040f43e542332fbf172f6b9944b7d62e389c0ec - languageName: node - linkType: hard - "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -9065,13 +7589,6 @@ __metadata: languageName: node linkType: hard -"is-unix@npm:~2.0.1": - version: 2.0.7 - resolution: "is-unix@npm:2.0.7" - checksum: 10/64a6f0bb1e4fe2a4d152484073547c5d55e66b4b7bc9857e5074d251c8d6efbf524249e3663c79f5b98034348b8cc07a36770ebe86fece2635c9c283ab60fc59 - languageName: node - linkType: hard - "is-weakref@npm:^1.0.2": version: 1.0.2 resolution: "is-weakref@npm:1.0.2" @@ -9097,16 +7614,6 @@ __metadata: languageName: node linkType: hard -"isomorphic-unfetch@npm:^4.0.2": - version: 4.0.2 - resolution: "isomorphic-unfetch@npm:4.0.2" - dependencies: - node-fetch: "npm:^3.2.0" - unfetch: "npm:^5.0.0" - checksum: 10/53561c3e42de8b1d6719563906d0e04367b3cc55b5eb2e5fc1dbd6445ae4a79f914d481716ab5f2ff188e2df45c730bfcc610364df24844514862f52760c14fd - languageName: node - linkType: hard - "jiti@npm:^1.18.2": version: 1.18.2 resolution: "jiti@npm:1.18.2" @@ -9160,13 +7667,6 @@ __metadata: languageName: node linkType: hard -"json-parse-better-errors@npm:^1.0.1": - version: 1.0.2 - resolution: "json-parse-better-errors@npm:1.0.2" - checksum: 10/5553232045359b767b0f2039a6777fede1a8d7dca1a0ffb1f9ef73a7519489ae7f566b2e040f2b4c38edb8e35e37ae07af7f0a52420902f869ee0dbf5dc6c784 - languageName: node - linkType: hard - "json-schema-traverse@npm:^0.4.1": version: 0.4.1 resolution: "json-schema-traverse@npm:0.4.1" @@ -9240,15 +7740,6 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.0.0": - version: 4.5.2 - resolution: "keyv@npm:4.5.2" - dependencies: - json-buffer: "npm:3.0.1" - checksum: 10/fbe6068cb46cfbf37b46f4a80e484a5e9c48c9a1eb09d9cb89382db6e12b801b60f07268ec8d7fa8d49f1f1e77badc5820c3135d478022df42691890a4c37038 - languageName: node - linkType: hard - "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -9344,18 +7835,6 @@ __metadata: languageName: node linkType: hard -"load-json-file@npm:^4.0.0": - version: 4.0.0 - resolution: "load-json-file@npm:4.0.0" - dependencies: - graceful-fs: "npm:^4.1.2" - parse-json: "npm:^4.0.0" - pify: "npm:^3.0.0" - strip-bom: "npm:^3.0.0" - checksum: 10/8f5d6d93ba64a9620445ee9bde4d98b1eac32cf6c8c2d20d44abfa41a6945e7969456ab5f1ca2fb06ee32e206c9769a20eec7002fe290de462e8c884b6b8b356 - languageName: node - linkType: hard - "load-tsconfig@npm:^0.2.3": version: 0.2.5 resolution: "load-tsconfig@npm:0.2.5" @@ -9451,13 +7930,6 @@ __metadata: languageName: node linkType: hard -"lowercase-keys@npm:^2.0.0": - version: 2.0.0 - resolution: "lowercase-keys@npm:2.0.0" - checksum: 10/1c233d2da35056e8c49fae8097ee061b8c799b2f02e33c2bf32f9913c7de8fb481ab04dab7df35e94156c800f5f34e99acbf32b21781d87c3aa43ef7b748b79e - languageName: node - linkType: hard - "lowercase-keys@npm:^3.0.0": version: 3.0.0 resolution: "lowercase-keys@npm:3.0.0" @@ -9465,16 +7937,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^4.0.1": - version: 4.1.5 - resolution: "lru-cache@npm:4.1.5" - dependencies: - pseudomap: "npm:^1.0.2" - yallist: "npm:^2.1.2" - checksum: 10/9ec7d73f11a32cba0e80b7a58fdf29970814c0c795acaee1a6451ddfd609bae6ef9df0837f5bbeabb571ecd49c1e2d79e10e9b4ed422cfba17a0cb6145b018a9 - languageName: node - linkType: hard - "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -9516,16 +7978,6 @@ __metadata: languageName: node linkType: hard -"m3u8stream@npm:^0.8.6": - version: 0.8.6 - resolution: "m3u8stream@npm:0.8.6" - dependencies: - miniget: "npm:^4.2.2" - sax: "npm:^1.2.4" - checksum: 10/6efb90aa392caf24142eafb88e6f9204655c859fbb3a61f2e6d81d0298d6357a962f392423187f686416bd630733ce461d0d45198a9c0694195dda59d79ec492 - languageName: node - linkType: hard - "magic-string@npm:^0.30.1": version: 0.30.5 resolution: "magic-string@npm:0.30.5" @@ -9535,15 +7987,6 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^3.1.0": - version: 3.1.0 - resolution: "make-dir@npm:3.1.0" - dependencies: - semver: "npm:^6.0.0" - checksum: 10/484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78 - languageName: node - linkType: hard - "make-fetch-happen@npm:^10.0.3": version: 10.2.1 resolution: "make-fetch-happen@npm:10.2.1" @@ -9568,13 +8011,6 @@ __metadata: languageName: node linkType: hard -"map-stream@npm:~0.1.0": - version: 0.1.0 - resolution: "map-stream@npm:0.1.0" - checksum: 10/f04a07041dccdf8140a4a6613e4731e917153ee031d3c837cb32ea7d609e8fbea538c44053718772f59dd1dca0ce68a5689ad006688612ee720d78bacf5bf24d - languageName: node - linkType: hard - "markdown-extensions@npm:^1.0.0": version: 1.1.1 resolution: "markdown-extensions@npm:1.1.1" @@ -9843,242 +8279,17 @@ __metadata: languageName: node linkType: hard -"mdast@npm:^3.0.0": - version: 3.0.0 - resolution: "mdast@npm:3.0.0" - checksum: 10/92acc29825657d45677c26a358a7ec1c91e16c037545c1619baee9d22457eef967d9ae3e8551d968bf0af7fe443f24dcceda24a3b9473b094fed29fe9d285fda - languageName: node - linkType: hard - -"media-typer@npm:0.3.0": - version: 0.3.0 - resolution: "media-typer@npm:0.3.0" - checksum: 10/38e0984db39139604756903a01397e29e17dcb04207bb3e081412ce725ab17338ecc47220c1b186b6bbe79a658aad1b0d41142884f5a481f36290cdefbe6aa46 - languageName: node - linkType: hard - -"mediaplex-android-arm64@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-android-arm64@npm:0.0.7" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"mediaplex-android-arm64@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-android-arm64@npm:0.0.9" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"mediaplex-darwin-arm64@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-darwin-arm64@npm:0.0.7" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"mediaplex-darwin-arm64@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-darwin-arm64@npm:0.0.9" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"mediaplex-darwin-universal@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-darwin-universal@npm:0.0.7" - conditions: os=darwin - languageName: node - linkType: hard - -"mediaplex-darwin-universal@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-darwin-universal@npm:0.0.9" - conditions: os=darwin - languageName: node - linkType: hard - -"mediaplex-darwin-x64@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-darwin-x64@npm:0.0.7" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"mediaplex-darwin-x64@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-darwin-x64@npm:0.0.9" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"mediaplex-freebsd-x64@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-freebsd-x64@npm:0.0.7" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"mediaplex-freebsd-x64@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-freebsd-x64@npm:0.0.9" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"mediaplex-linux-arm-gnueabihf@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-linux-arm-gnueabihf@npm:0.0.7" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"mediaplex-linux-arm-gnueabihf@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-linux-arm-gnueabihf@npm:0.0.9" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"mediaplex-linux-x64-gnu@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-linux-x64-gnu@npm:0.0.7" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"mediaplex-linux-x64-gnu@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-linux-x64-gnu@npm:0.0.9" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"mediaplex-win32-arm64-msvc@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-win32-arm64-msvc@npm:0.0.7" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"mediaplex-win32-arm64-msvc@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-win32-arm64-msvc@npm:0.0.9" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"mediaplex-win32-ia32-msvc@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-win32-ia32-msvc@npm:0.0.7" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"mediaplex-win32-ia32-msvc@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-win32-ia32-msvc@npm:0.0.9" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"mediaplex-win32-x64-msvc@npm:0.0.7": - version: 0.0.7 - resolution: "mediaplex-win32-x64-msvc@npm:0.0.7" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"mediaplex-win32-x64-msvc@npm:0.0.9": - version: 0.0.9 - resolution: "mediaplex-win32-x64-msvc@npm:0.0.9" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"mediaplex@npm:^0.0.7": - version: 0.0.7 - resolution: "mediaplex@npm:0.0.7" - dependencies: - mediaplex-android-arm64: "npm:0.0.7" - mediaplex-darwin-arm64: "npm:0.0.7" - mediaplex-darwin-universal: "npm:0.0.7" - mediaplex-darwin-x64: "npm:0.0.7" - mediaplex-freebsd-x64: "npm:0.0.7" - mediaplex-linux-arm-gnueabihf: "npm:0.0.7" - mediaplex-linux-x64-gnu: "npm:0.0.7" - mediaplex-win32-arm64-msvc: "npm:0.0.7" - mediaplex-win32-ia32-msvc: "npm:0.0.7" - mediaplex-win32-x64-msvc: "npm:0.0.7" - dependenciesMeta: - mediaplex-android-arm64: - optional: true - mediaplex-darwin-arm64: - optional: true - mediaplex-darwin-universal: - optional: true - mediaplex-darwin-x64: - optional: true - mediaplex-freebsd-x64: - optional: true - mediaplex-linux-arm-gnueabihf: - optional: true - mediaplex-linux-x64-gnu: - optional: true - mediaplex-win32-arm64-msvc: - optional: true - mediaplex-win32-ia32-msvc: - optional: true - mediaplex-win32-x64-msvc: - optional: true - checksum: 10/54bdd39f199682007a8a0f641387c3e6b684cd51ba00a6ae073cc9f4882d59b217e6f66cc8b6bccb189585836fbcd2b7a5579a36aa7010dd21774c27cdeb666f - languageName: node - linkType: hard - -"mediaplex@npm:^0.0.9": - version: 0.0.9 - resolution: "mediaplex@npm:0.0.9" - dependencies: - mediaplex-android-arm64: "npm:0.0.9" - mediaplex-darwin-arm64: "npm:0.0.9" - mediaplex-darwin-universal: "npm:0.0.9" - mediaplex-darwin-x64: "npm:0.0.9" - mediaplex-freebsd-x64: "npm:0.0.9" - mediaplex-linux-arm-gnueabihf: "npm:0.0.9" - mediaplex-linux-x64-gnu: "npm:0.0.9" - mediaplex-win32-arm64-msvc: "npm:0.0.9" - mediaplex-win32-ia32-msvc: "npm:0.0.9" - mediaplex-win32-x64-msvc: "npm:0.0.9" - dependenciesMeta: - mediaplex-android-arm64: - optional: true - mediaplex-darwin-arm64: - optional: true - mediaplex-darwin-universal: - optional: true - mediaplex-darwin-x64: - optional: true - mediaplex-freebsd-x64: - optional: true - mediaplex-linux-arm-gnueabihf: - optional: true - mediaplex-linux-x64-gnu: - optional: true - mediaplex-win32-arm64-msvc: - optional: true - mediaplex-win32-ia32-msvc: - optional: true - mediaplex-win32-x64-msvc: - optional: true - checksum: 10/1e922f7a0526369bf8c4755a7f225ae6135cd33f1d5712648c8773cb117c21ccc3a6d8e21a27058201acf56bdf3067596dee858ec8f9324f213daddf9d4fcb42 - languageName: node - linkType: hard - -"memorystream@npm:^0.3.1": - version: 0.3.1 - resolution: "memorystream@npm:0.3.1" - checksum: 10/2e34a1e35e6eb2e342f788f75f96c16f115b81ff6dd39e6c2f48c78b464dbf5b1a4c6ebfae4c573bd0f8dbe8c57d72bb357c60523be184655260d25855c03902 +"mdast@npm:^3.0.0": + version: 3.0.0 + resolution: "mdast@npm:3.0.0" + checksum: 10/92acc29825657d45677c26a358a7ec1c91e16c037545c1619baee9d22457eef967d9ae3e8551d968bf0af7fe443f24dcceda24a3b9473b094fed29fe9d285fda + languageName: node + linkType: hard + +"media-typer@npm:0.3.0": + version: 0.3.0 + resolution: "media-typer@npm:0.3.0" + checksum: 10/38e0984db39139604756903a01397e29e17dcb04207bb3e081412ce725ab17338ecc47220c1b186b6bbe79a658aad1b0d41142884f5a481f36290cdefbe6aa46 languageName: node linkType: hard @@ -10606,7 +8817,7 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:1.52.0, mime-db@npm:^1.28.0": +"mime-db@npm:1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" checksum: 10/54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7 @@ -10645,13 +8856,6 @@ __metadata: languageName: node linkType: hard -"mimic-response@npm:^1.0.0": - version: 1.0.1 - resolution: "mimic-response@npm:1.0.1" - checksum: 10/034c78753b0e622bc03c983663b1cdf66d03861050e0c8606563d149bc2b02d63f62ce4d32be4ab50d0553ae0ffe647fc34d1f5281184c6e1e8cf4d85e8d9823 - languageName: node - linkType: hard - "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -10666,20 +8870,6 @@ __metadata: languageName: node linkType: hard -"miniget@npm:^4.2.2": - version: 4.2.2 - resolution: "miniget@npm:4.2.2" - checksum: 10/6a420fa99774ee7cbf8febdb6b9e72246f7e235c13245778864e11b119f501aa5f1d79acc0565fbc46c082b5e02b2092aaddec4245c644e6a9ba2b9b99e05f6c - languageName: node - linkType: hard - -"miniget@npm:^4.2.3": - version: 4.2.3 - resolution: "miniget@npm:4.2.3" - checksum: 10/fac25ed0b64e72d6383698f7416b6e1f23f91a48cd9a2abea8b4d284784329cc1442346ae0fddd3b69d2a846122997eb22af4b0b9176f25efed6d6dce9c1d6e0 - languageName: node - linkType: hard - "minimatch@npm:^3.0.4": version: 3.0.4 resolution: "minimatch@npm:3.0.4" @@ -10869,45 +9059,6 @@ __metadata: languageName: node linkType: hard -"music-bot@workspace:apps/music-bot": - version: 0.0.0-use.local - resolution: "music-bot@workspace:apps/music-bot" - dependencies: - "@discord-player/equalizer": "workspace:^" - "@discord-player/extractor": "workspace:^" - "@discord-player/utils": "workspace:^" - "@discordjs/opus": "npm:^0.9.0" - "@distube/ytdl-core": "npm:^4.13.3" - "@sapphire/discord.js-utilities": "npm:^6.0.3" - "@sapphire/duration": "npm:^1.0.0" - "@sapphire/framework": "npm:^4.2.1" - "@sapphire/plugin-api": "npm:^5.0.1" - "@sapphire/plugin-hmr": "npm:^2.0.0" - "@sapphire/plugin-logger": "npm:^3.0.1" - "@sapphire/prettier-config": "npm:^1.4.5" - "@sapphire/ts-config": "npm:^3.3.4" - "@skyra/env-utilities": "npm:^1.1.0" - "@swc/cli": "npm:^0.1.62" - "@swc/core": "npm:^1.3.37" - "@types/node": "npm:^18.14.6" - "@types/ws": "npm:^8.5.4" - colorette: "npm:^2.0.19" - discord-api-types: "npm:^0.37.35" - discord-player: "workspace:^" - mediaplex: "npm:^0.0.9" - npm-run-all: "npm:^4.1.5" - opusscript: "npm:^0.0.8" - play-dl: "npm:^1.9.7" - prettier: "npm:^2.8.4" - tsc-watch: "npm:^6.0.0" - tsup: "npm:^7.2.0" - tsx: "npm:^3.12.7" - typescript: "npm:^5.2.2" - vitest: "npm:^0.34.6" - youtube-ext: "npm:^1.1.23" - languageName: unknown - linkType: soft - "mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -11025,13 +9176,6 @@ __metadata: languageName: node linkType: hard -"nice-try@npm:^1.0.4": - version: 1.0.5 - resolution: "nice-try@npm:1.0.5" - checksum: 10/0b4af3b5bb5d86c289f7a026303d192a7eb4417231fe47245c460baeabae7277bcd8fd9c728fb6bd62c30b3e15cd6620373e2cf33353b095d8b403d3e8a15aff - languageName: node - linkType: hard - "nlcst-to-string@npm:^3.0.0": version: 3.1.1 resolution: "nlcst-to-string@npm:3.1.1" @@ -11041,29 +9185,6 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^5.0.0": - version: 5.1.0 - resolution: "node-addon-api@npm:5.1.0" - dependencies: - node-gyp: "npm:latest" - checksum: 10/595f59ffb4630564f587c502119cbd980d302e482781021f3b479f5fc7e41cf8f2f7280fdc2795f32d148e4f3259bd15043c52d4a3442796aa6f1ae97b959636 - languageName: node - linkType: hard - -"node-cleanup@npm:^2.1.2": - version: 2.1.2 - resolution: "node-cleanup@npm:2.1.2" - checksum: 10/eeb831d27d734179ca6aa7504a65fa0debd7c77a883c5dbea2849fb7ed8fa0a3fe3a346926c5b1aaaf5537fd801d03da0efcf20b28385d7150276a9e8a2127a5 - languageName: node - linkType: hard - -"node-domexception@npm:^1.0.0": - version: 1.0.0 - resolution: "node-domexception@npm:1.0.0" - checksum: 10/e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233 - languageName: node - linkType: hard - "node-fetch@npm:^2.0.0": version: 2.6.11 resolution: "node-fetch@npm:2.6.11" @@ -11078,20 +9199,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.0": - version: 2.6.8 - resolution: "node-fetch@npm:2.6.8" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10/497d0e24985ba59eee37c25d4515b263cd9c92febcdc8606288f1a516d127c70123ce524318b66abe55ccefabda2a1c2bee8d3538f1a234a71ceee599b5cdb55 - languageName: node - linkType: hard - "node-fetch@npm:^2.6.7": version: 2.6.12 resolution: "node-fetch@npm:2.6.12" @@ -11106,17 +9213,6 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^3.2.0": - version: 3.3.2 - resolution: "node-fetch@npm:3.3.2" - dependencies: - data-uri-to-buffer: "npm:^4.0.0" - fetch-blob: "npm:^3.1.4" - formdata-polyfill: "npm:^4.0.10" - checksum: 10/24207ca8c81231c7c59151840e3fded461d67a31cf3e3b3968e12201a42f89ce4a0b5fb7079b1fa0a4655957b1ca9257553200f03a9f668b45ebad265ca5593d - languageName: node - linkType: hard - "node-gyp@npm:latest": version: 9.3.1 resolution: "node-gyp@npm:9.3.1" @@ -11137,26 +9233,6 @@ __metadata: languageName: node linkType: hard -"node-html-parser@npm:^6.1.4": - version: 6.1.4 - resolution: "node-html-parser@npm:6.1.4" - dependencies: - css-select: "npm:^5.1.0" - he: "npm:1.2.0" - checksum: 10/c97fa6593ca8f1eac4e2de4897568b7a4ef7328b642247ecc3037df24846f383d1db68eef264568d64b0ce71d0e89ebe3025769061e7af6f63fe4eae19f05c94 - languageName: node - linkType: hard - -"node-html-parser@npm:^6.1.9": - version: 6.1.10 - resolution: "node-html-parser@npm:6.1.10" - dependencies: - css-select: "npm:^5.1.0" - he: "npm:1.2.0" - checksum: 10/3b618a7616eeba4c4ecdcd023232baf7f22e22726db0592fe4d85817401be51e62646e6f75eacb361936209afc42318b0bc0978cf7f8b9cdf3a4642a19ad5883 - languageName: node - linkType: hard - "node-releases@npm:^2.0.12": version: 2.0.12 resolution: "node-releases@npm:2.0.12" @@ -11164,17 +9240,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^5.0.0": - version: 5.0.0 - resolution: "nopt@npm:5.0.0" - dependencies: - abbrev: "npm:1" - bin: - nopt: bin/nopt.js - checksum: 10/00f9bb2d16449469ba8ffcf9b8f0eae6bae285ec74b135fec533e5883563d2400c0cd70902d0a7759e47ac031ccf206ace4e86556da08ed3f1c66dda206e9ccd - languageName: node - linkType: hard - "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -11186,18 +9251,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^2.3.2": - version: 2.5.0 - resolution: "normalize-package-data@npm:2.5.0" - dependencies: - hosted-git-info: "npm:^2.1.4" - resolve: "npm:^1.10.0" - semver: "npm:2 || 3 || 4 || 5" - validate-npm-package-license: "npm:^3.0.1" - checksum: 10/644f830a8bb9b7cc9bf2f6150618727659ee27cdd0840d1c1f97e8e6cab0803a098a2c19f31c6247ad9d3a0792e61521a13a6e8cd87cc6bb676e3150612c03d4 - languageName: node - linkType: hard - "normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -11212,13 +9265,6 @@ __metadata: languageName: node linkType: hard -"normalize-url@npm:^6.0.1": - version: 6.1.0 - resolution: "normalize-url@npm:6.1.0" - checksum: 10/5ae699402c9d5ffa330adc348fcd6fc6e6a155ab7c811b96e30b7ecab60ceef821d8f86443869671dda71bbc47f4b9625739c82ad247e883e9aefe875bfb8659 - languageName: node - linkType: hard - "normalize-url@npm:^8.0.0": version: 8.0.1 resolution: "normalize-url@npm:8.0.1" @@ -11226,36 +9272,6 @@ __metadata: languageName: node linkType: hard -"npm-run-all@npm:^4.1.5": - version: 4.1.5 - resolution: "npm-run-all@npm:4.1.5" - dependencies: - ansi-styles: "npm:^3.2.1" - chalk: "npm:^2.4.1" - cross-spawn: "npm:^6.0.5" - memorystream: "npm:^0.3.1" - minimatch: "npm:^3.0.4" - pidtree: "npm:^0.3.0" - read-pkg: "npm:^3.0.0" - shell-quote: "npm:^1.6.1" - string.prototype.padend: "npm:^3.0.0" - bin: - npm-run-all: bin/npm-run-all/index.js - run-p: bin/run-p/index.js - run-s: bin/run-s/index.js - checksum: 10/46020e92813223d015f4178cce5a2338164be5f25b0c391e256c0e84ac082544986c220013f1be7f002dcac07b81c7ee0cb5c5c30b84fd6ebb6de96a8d713745 - languageName: node - linkType: hard - -"npm-run-path@npm:^2.0.0": - version: 2.0.2 - resolution: "npm-run-path@npm:2.0.2" - dependencies: - path-key: "npm:^2.0.0" - checksum: 10/acd5ad81648ba4588ba5a8effb1d98d2b339d31be16826a118d50f182a134ac523172101b82eab1d01cb4c2ba358e857d54cfafd8163a1ffe7bd52100b741125 - languageName: node - linkType: hard - "npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" @@ -11274,18 +9290,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^5.0.1": - version: 5.0.1 - resolution: "npmlog@npm:5.0.1" - dependencies: - are-we-there-yet: "npm:^2.0.0" - console-control-strings: "npm:^1.1.0" - gauge: "npm:^3.0.0" - set-blocking: "npm:^2.0.0" - checksum: 10/f42c7b9584cdd26a13c41a21930b6f5912896b6419ab15be88cc5721fc792f1c3dd30eb602b26ae08575694628ba70afdcf3675d86e4f450fc544757e52726ec - languageName: node - linkType: hard - "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -11298,15 +9302,6 @@ __metadata: languageName: node linkType: hard -"nth-check@npm:^2.0.1": - version: 2.1.1 - resolution: "nth-check@npm:2.1.1" - dependencies: - boolbase: "npm:^1.0.0" - checksum: 10/5afc3dafcd1573b08877ca8e6148c52abd565f1d06b1eb08caf982e3fa289a82f2cae697ffb55b5021e146d60443f1590a5d6b944844e944714a5b549675bcd3 - languageName: node - linkType: hard - "object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" @@ -11399,7 +9394,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": +"once@npm:^1.3.0, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -11494,15 +9489,6 @@ __metadata: languageName: node linkType: hard -"os-filter-obj@npm:^2.0.0": - version: 2.0.0 - resolution: "os-filter-obj@npm:2.0.0" - dependencies: - arch: "npm:^2.1.0" - checksum: 10/08808a109b2dba9be8686cc006e082a0f6595e6d87e2a30e4147cb1d22b62a30a6e5f4fd78226aee76d9158c84db3cea292adec02e6591452e93cb33bf5da877 - languageName: node - linkType: hard - "p-any@npm:^4.0.0": version: 4.0.0 resolution: "p-any@npm:4.0.0" @@ -11513,13 +9499,6 @@ __metadata: languageName: node linkType: hard -"p-cancelable@npm:^2.0.0": - version: 2.1.1 - resolution: "p-cancelable@npm:2.1.1" - checksum: 10/7f1b64db17fc54acf359167d62898115dcf2a64bf6b3b038e4faf36fc059e5ed762fb9624df8ed04b25bee8de3ab8d72dea9879a2a960cd12e23c420a4aca6ed - languageName: node - linkType: hard - "p-cancelable@npm:^3.0.0": version: 3.0.0 resolution: "p-cancelable@npm:3.0.0" @@ -11527,13 +9506,6 @@ __metadata: languageName: node linkType: hard -"p-finally@npm:^1.0.0": - version: 1.0.0 - resolution: "p-finally@npm:1.0.0" - checksum: 10/93a654c53dc805dd5b5891bab16eb0ea46db8f66c4bfd99336ae929323b1af2b70a8b0654f8f1eae924b2b73d037031366d645f1fd18b3d30cbd15950cc4b1d4 - languageName: node - linkType: hard - "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -11612,16 +9584,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-json@npm:4.0.0" - dependencies: - error-ex: "npm:^1.3.1" - json-parse-better-errors: "npm:^1.0.1" - checksum: 10/0fe227d410a61090c247e34fa210552b834613c006c2c64d9a05cfe9e89cf8b4246d1246b1a99524b53b313e9ac024438d0680f67e33eaed7e6f38db64cfe7b5 - languageName: node - linkType: hard - "parse-latin@npm:^5.0.0": version: 5.0.1 resolution: "parse-latin@npm:5.0.1" @@ -11670,13 +9632,6 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^2.0.0, path-key@npm:^2.0.1": - version: 2.0.1 - resolution: "path-key@npm:2.0.1" - checksum: 10/6e654864e34386a2a8e6bf72cf664dcabb76574dd54013add770b374384d438aca95f4357bb26935b514a4e4c2c9b19e191f2200b282422a76ee038b9258c5e7 - languageName: node - linkType: hard - "path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -11705,15 +9660,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^3.0.0": - version: 3.0.0 - resolution: "path-type@npm:3.0.0" - dependencies: - pify: "npm:^3.0.0" - checksum: 10/735b35e256bad181f38fa021033b1c33cfbe62ead42bb2222b56c210e42938eecb272ae1949f3b6db4ac39597a61b44edd8384623ec4d79bfdc9a9c0f12537a6 - languageName: node - linkType: hard - "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -11742,22 +9688,6 @@ __metadata: languageName: node linkType: hard -"pause-stream@npm:0.0.11": - version: 0.0.11 - resolution: "pause-stream@npm:0.0.11" - dependencies: - through: "npm:~2.3" - checksum: 10/1407efadfe814b5c487e4b28d6139cb7e03ee5d25fbb5f89a68f2053e81f05ce6b2bec196eeb3d46ef2c856f785016d14816b0d0e3c3abd1b64311c5c20660dc - languageName: node - linkType: hard - -"peek-readable@npm:^4.1.0": - version: 4.1.0 - resolution: "peek-readable@npm:4.1.0" - checksum: 10/97373215dcf382748645c3d22ac5e8dbd31759f7bd0c539d9fdbaaa7d22021838be3e55110ad0ed8f241c489342304b14a50dfee7ef3bcee2987d003b24ecc41 - languageName: node - linkType: hard - "peek-readable@npm:^5.0.0": version: 5.0.0 resolution: "peek-readable@npm:5.0.0" @@ -11790,29 +9720,13 @@ __metadata: languageName: node linkType: hard -"pidtree@npm:^0.3.0": - version: 0.3.1 - resolution: "pidtree@npm:0.3.1" - bin: - pidtree: bin/pidtree.js - checksum: 10/eb85b841cd168151bfadb984f9514d67a884d6962d4a2d250d4e8acf85cf031d7dab080f7272fb2735f9033364e5058c73eeebbee3cf6fd829169a75d19f189a - languageName: node - linkType: hard - -"pify@npm:^2.2.0, pify@npm:^2.3.0": +"pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" checksum: 10/9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba languageName: node linkType: hard -"pify@npm:^3.0.0": - version: 3.0.0 - resolution: "pify@npm:3.0.0" - checksum: 10/668c1dc8d9fc1b34b9ce3b16ba59deb39d4dc743527bf2ed908d2b914cb8ba40aa5ba6960b27c417c241531c5aafd0598feeac2d50cb15278cf9863fa6b02a77 - languageName: node - linkType: hard - "pirates@npm:^4.0.1": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -11831,31 +9745,6 @@ __metadata: languageName: node linkType: hard -"play-audio@npm:^0.5.2": - version: 0.5.2 - resolution: "play-audio@npm:0.5.2" - checksum: 10/c636d419c92f48ce642918360258ffba43ceef2fa3463eb4f1b98b3fdd41dbfb079750895e44cd5b5b957ee7a80b5301f06e527266667fe8f174f44e28a3c3ce - languageName: node - linkType: hard - -"play-dl@npm:^1.9.6": - version: 1.9.6 - resolution: "play-dl@npm:1.9.6" - dependencies: - play-audio: "npm:^0.5.2" - checksum: 10/4c3d9e9c51edca71d5effdf1d1ef8610558cb60d8c9835d4c4a35669c1729cbced6e550a4e3e48ba798da6bb2198035601669818306d296bbf5e18f5cd497968 - languageName: node - linkType: hard - -"play-dl@npm:^1.9.7": - version: 1.9.7 - resolution: "play-dl@npm:1.9.7" - dependencies: - play-audio: "npm:^0.5.2" - checksum: 10/6187e04dbd1f5f41beb6ff995bb746c78152f038f9a84963f1fbdd2ecb48ff8a5a713ad2985108a1d3cd7d01c9b689eee6ff2bb4a61176e4a988729a2438bca2 - languageName: node - linkType: hard - "postcss-import@npm:^15.1.0": version: 15.1.0 resolution: "postcss-import@npm:15.1.0" @@ -11975,15 +9864,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.8.4": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" - bin: - prettier: bin-prettier.js - checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 - languageName: node - linkType: hard - "pretty-format@npm:^29.5.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -12080,31 +9960,6 @@ __metadata: languageName: node linkType: hard -"ps-tree@npm:^1.2.0": - version: 1.2.0 - resolution: "ps-tree@npm:1.2.0" - dependencies: - event-stream: "npm:=3.3.4" - bin: - ps-tree: ./bin/ps-tree.js - checksum: 10/0587defdc20c0768fad884623c0204c77e5228878a5cb043676b00529220ec12d9cb6a328a0580767a9909a317bff466fe4530a4676e3d145a9deb3b7fbbeef3 - languageName: node - linkType: hard - -"pseudomap@npm:^1.0.2": - version: 1.0.2 - resolution: "pseudomap@npm:1.0.2" - checksum: 10/856c0aae0ff2ad60881168334448e898ad7a0e45fe7386d114b150084254c01e200c957cf378378025df4e052c7890c5bd933939b0e0d2ecfcc1dc2f0b2991f5 - languageName: node - linkType: hard - -"psl@npm:^1.1.33": - version: 1.9.0 - resolution: "psl@npm:1.9.0" - checksum: 10/d07879d4bfd0ac74796306a8e5a36a93cfb9c4f4e8ee8e63fbb909066c192fe1008cd8f12abd8ba2f62ca28247949a20c8fb32e1d18831d9e71285a1569720f9 - languageName: node - linkType: hard - "public-ip@npm:^5.0.0": version: 5.0.0 resolution: "public-ip@npm:5.0.0" @@ -12116,16 +9971,6 @@ __metadata: languageName: node linkType: hard -"pump@npm:^3.0.0": - version: 3.0.0 - resolution: "pump@npm:3.0.0" - dependencies: - end-of-stream: "npm:^1.1.0" - once: "npm:^1.3.1" - checksum: 10/e42e9229fba14732593a718b04cb5e1cfef8254544870997e0ecd9732b189a48e1256e4e5478148ecb47c8511dca2b09eae56b4d0aad8009e6fac8072923cfc9 - languageName: node - linkType: hard - "punycode@npm:^2.1.0": version: 2.1.1 resolution: "punycode@npm:2.1.1" @@ -12133,13 +9978,6 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.1": - version: 2.3.0 - resolution: "punycode@npm:2.3.0" - checksum: 10/d4e7fbb96f570c57d64b09a35a1182c879ac32833de7c6926a2c10619632c1377865af3dab5479f59d51da18bcd5035a20a5ef6ceb74020082a3e78025d9a9ca - languageName: node - linkType: hard - "qs@npm:6.11.0": version: 6.11.0 resolution: "qs@npm:6.11.0" @@ -12149,13 +9987,6 @@ __metadata: languageName: node linkType: hard -"querystringify@npm:^2.1.1": - version: 2.2.0 - resolution: "querystringify@npm:2.2.0" - checksum: 10/46ab16f252fd892fc29d6af60966d338cdfeea68a231e9457631ffd22d67cec1e00141e0a5236a2eb16c0d7d74175d9ec1d6f963660c6f2b1c2fc85b194c5680 - languageName: node - linkType: hard - "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -12332,17 +10163,6 @@ __metadata: languageName: node linkType: hard -"read-pkg@npm:^3.0.0": - version: 3.0.0 - resolution: "read-pkg@npm:3.0.0" - dependencies: - load-json-file: "npm:^4.0.0" - normalize-package-data: "npm:^2.3.2" - path-type: "npm:^3.0.0" - checksum: 10/398903ebae6c7e9965419a1062924436cc0b6f516c42c4679a90290d2f87448ed8f977e7aa2dbba4aa1ac09248628c43e493ac25b2bc76640e946035200e34c6 - languageName: node - linkType: hard - "readable-stream@npm:^3.4.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" @@ -12365,7 +10185,7 @@ __metadata: languageName: node linkType: hard -"readable-web-to-node-stream@npm:^3.0.0, readable-web-to-node-stream@npm:^3.0.2": +"readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" dependencies: @@ -12551,14 +10371,7 @@ __metadata: languageName: node linkType: hard -"requires-port@npm:^1.0.0": - version: 1.0.0 - resolution: "requires-port@npm:1.0.0" - checksum: 10/878880ee78ccdce372784f62f52a272048e2d0827c29ae31e7f99da18b62a2b9463ea03a75f277352f4697c100183debb0532371ad515a2d49d4bfe596dd4c20 - languageName: node - linkType: hard - -"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": +"resolve-alpn@npm:^1.2.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" checksum: 10/744e87888f0b6fa0b256ab454ca0b9c0b80808715e2ef1f3672773665c92a941f6181194e30ccae4a8cd0adbe0d955d3f133102636d2ee0cca0119fec0bc9aec @@ -12586,7 +10399,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.22.2": +"resolve@npm:^1.1.7, resolve@npm:^1.22.2": version: 1.22.3 resolution: "resolve@npm:1.22.3" dependencies: @@ -12625,7 +10438,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin": +"resolve@patch:resolve@npm%3A^1.1.7#optional!builtin, resolve@patch:resolve@npm%3A^1.22.2#optional!builtin": version: 1.22.3 resolution: "resolve@patch:resolve@npm%3A1.22.3#optional!builtin::version=1.22.3&hash=c3c19d" dependencies: @@ -12664,15 +10477,6 @@ __metadata: languageName: node linkType: hard -"responselike@npm:^2.0.0": - version: 2.0.1 - resolution: "responselike@npm:2.0.1" - dependencies: - lowercase-keys: "npm:^2.0.0" - checksum: 10/b122535466e9c97b55e69c7f18e2be0ce3823c5d47ee8de0d9c0b114aa55741c6db8bfbfce3766a94d1272e61bfb1ebf0a15e9310ac5629fbb7446a861b4fd3a - languageName: node - linkType: hard - "responselike@npm:^3.0.0": version: 3.0.0 resolution: "responselike@npm:3.0.0" @@ -12753,15 +10557,6 @@ __metadata: languageName: node linkType: hard -"reverbnation-scraper@npm:^2.0.0": - version: 2.0.0 - resolution: "reverbnation-scraper@npm:2.0.0" - dependencies: - node-fetch: "npm:^2.6.0" - checksum: 10/e1b9894f5cb1628d1d32732ef4d6fe76e112f6dfb87fbf7dd36d1ab8117d65555ddcd9a94cf1de4259c82d6439c6ee4029857206c9831b766e411108b4401cdd - languageName: node - linkType: hard - "rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -12905,13 +10700,6 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.1.3, sax@npm:^1.2.4": - version: 1.2.4 - resolution: "sax@npm:1.2.4" - checksum: 10/09b79ff6dc09689a24323352117c94593c69db348997b2af0edbd82fa08aba47d778055bf9616b57285bb73d25d790900c044bf631a8f10c8252412e3f3fe5dd - languageName: node - linkType: hard - "scheduler@npm:^0.23.0": version: 0.23.0 resolution: "scheduler@npm:0.23.0" @@ -12931,32 +10719,7 @@ __metadata: languageName: node linkType: hard -"semver-regex@npm:^4.0.5": - version: 4.0.5 - resolution: "semver-regex@npm:4.0.5" - checksum: 10/b9e5c0573c4a997fb7e6e76321385d254797e86c8dba5e23f3cd8cf8f40b40414097a51514e5fead61dcb88ff10d3676355c01e2040f3c68f6c24bfd2073da2e - languageName: node - linkType: hard - -"semver-truncate@npm:^2.0.0": - version: 2.0.0 - resolution: "semver-truncate@npm:2.0.0" - dependencies: - semver: "npm:^6.0.0" - checksum: 10/713c2bd49add098c3fd6271091e7c8c13908ab3f052d58a19b68920da9f101d34eb6a0c60ef4bd5fa3c345f001e0df37bb831602082441bb35ba857cac42e0f4 - languageName: node - linkType: hard - -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0": - version: 5.7.1 - resolution: "semver@npm:5.7.1" - bin: - semver: ./bin/semver - checksum: 10/fbc71cf00736480ca0dd67f2527cda6e0fde5447af00bd2ce06cb522d510216603a63ed0c6c87d8904507c1a4e8113e628a71424ebd9e0fd7d345ee8ed249690 - languageName: node - linkType: hard - -"semver@npm:^6.0.0, semver@npm:^6.3.0": +"semver@npm:^6.3.0": version: 6.3.0 resolution: "semver@npm:6.3.0" bin: @@ -12987,17 +10750,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.8": - version: 7.5.2 - resolution: "semver@npm:7.5.2" - dependencies: - lru-cache: "npm:^6.0.0" - bin: - semver: bin/semver.js - checksum: 10/f77b3a1842e19b78e5864a175d62699a17c0c25f6223366684041e8c7dd6a55f0091887f405c534895dfe69e1d770528d072b32d9ed866ab24392fe34344d3b5 - languageName: node - linkType: hard - "semver@npm:^7.5.3": version: 7.5.3 resolution: "semver@npm:7.5.3" @@ -13145,28 +10897,12 @@ __metadata: languageName: node linkType: hard -"shebang-command@npm:^1.2.0": - version: 1.2.0 - resolution: "shebang-command@npm:1.2.0" - dependencies: - shebang-regex: "npm:^1.0.0" - checksum: 10/9eed1750301e622961ba5d588af2212505e96770ec376a37ab678f965795e995ade7ed44910f5d3d3cb5e10165a1847f52d3348c64e146b8be922f7707958908 - languageName: node - linkType: hard - "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" dependencies: - shebang-regex: "npm:^3.0.0" - checksum: 10/6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa - languageName: node - linkType: hard - -"shebang-regex@npm:^1.0.0": - version: 1.0.0 - resolution: "shebang-regex@npm:1.0.0" - checksum: 10/404c5a752cd40f94591dfd9346da40a735a05139dac890ffc229afba610854d8799aaa52f87f7e0c94c5007f2c6af55bdcaeb584b56691926c5eaf41dc8f1372 + shebang-regex: "npm:^3.0.0" + checksum: 10/6b52fe87271c12968f6a054e60f6bde5f0f3d2db483a1e5c3e12d657c488a15474121a1d55cd958f6df026a54374ec38a4a963988c213b7570e1d51575cea7fa languageName: node linkType: hard @@ -13177,13 +10913,6 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.6.1": - version: 1.8.1 - resolution: "shell-quote@npm:1.8.1" - checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d - languageName: node - linkType: hard - "shiki@npm:^0.14.7": version: 0.14.7 resolution: "shiki@npm:0.14.7" @@ -13214,31 +10943,13 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 languageName: node linkType: hard -"simple-concat@npm:^1.0.0": - version: 1.0.1 - resolution: "simple-concat@npm:1.0.1" - checksum: 10/4d211042cc3d73a718c21ac6c4e7d7a0363e184be6a5ad25c8a1502e49df6d0a0253979e3d50dbdd3f60ef6c6c58d756b5d66ac1e05cda9cacd2e9fc59e3876a - languageName: node - linkType: hard - -"simple-get@npm:~4.0.1": - version: 4.0.1 - resolution: "simple-get@npm:4.0.1" - dependencies: - decompress-response: "npm:^6.0.0" - once: "npm:^1.3.1" - simple-concat: "npm:^1.0.0" - checksum: 10/93f1b32319782f78f2f2234e9ce34891b7ab6b990d19d8afefaa44423f5235ce2676aae42d6743fecac6c8dfff4b808d4c24fe5265be813d04769917a9a44f36 - languageName: node - linkType: hard - "simple-swizzle@npm:^0.2.2": version: 0.2.2 resolution: "simple-swizzle@npm:0.2.2" @@ -13248,7 +10959,7 @@ __metadata: languageName: node linkType: hard -"slash@npm:3.0.0, slash@npm:^3.0.0": +"slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" checksum: 10/94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c @@ -13325,33 +11036,6 @@ __metadata: languageName: node linkType: hard -"sort-keys-length@npm:^1.0.0": - version: 1.0.1 - resolution: "sort-keys-length@npm:1.0.1" - dependencies: - sort-keys: "npm:^1.0.0" - checksum: 10/f9acac5fb31580a9e3d43b419dc86a1b75e85b79036a084d95dd4d1062b621c9589906588ac31e370a0dd381be46d8dbe900efa306d087ca9c912d7a59b5a590 - languageName: node - linkType: hard - -"sort-keys@npm:^1.0.0": - version: 1.1.2 - resolution: "sort-keys@npm:1.1.2" - dependencies: - is-plain-obj: "npm:^1.0.0" - checksum: 10/0ac2ea2327d92252f07aa7b2f8c7023a1f6ce3306439a3e81638cce9905893c069521d168f530fb316d1a929bdb052b742969a378190afaef1bc64fa69e29576 - languageName: node - linkType: hard - -"soundcloud.ts@npm:^0.5.2": - version: 0.5.2 - resolution: "soundcloud.ts@npm:0.5.2" - dependencies: - undici: "npm:^5.22.1" - checksum: 10/db4df8921415087d99d8522552d137ebc176a95ef476c19d327a93960f745a4e4ceaa6fe3b4f1e035f094608128fdab23cad96a9561380812cab7cb238ce310e - languageName: node - linkType: hard - "source-map-js@npm:^1.0.2": version: 1.0.2 resolution: "source-map-js@npm:1.0.2" @@ -13359,16 +11043,6 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.21": - version: 0.5.21 - resolution: "source-map-support@npm:0.5.21" - dependencies: - buffer-from: "npm:^1.0.0" - source-map: "npm:^0.6.0" - checksum: 10/8317e12d84019b31e34b86d483dd41d6f832f389f7417faf8fc5c75a66a12d9686e47f589a0554a868b8482f037e23df9d040d29387eb16fa14cb85f091ba207 - languageName: node - linkType: hard - "source-map@npm:0.8.0-beta.0": version: 0.8.0-beta.0 resolution: "source-map@npm:0.8.0-beta.0" @@ -13378,14 +11052,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0": - version: 0.6.1 - resolution: "source-map@npm:0.6.1" - checksum: 10/59ef7462f1c29d502b3057e822cdbdae0b0e565302c4dd1a95e11e793d8d9d62006cdc10e0fd99163ca33ff2071360cf50ee13f90440806e7ed57d81cba2f7ff - languageName: node - linkType: hard - -"source-map@npm:^0.7.0, source-map@npm:^0.7.3": +"source-map@npm:^0.7.0": version: 0.7.4 resolution: "source-map@npm:0.7.4" checksum: 10/a0f7c9b797eda93139842fd28648e868a9a03ea0ad0d9fa6602a0c1f17b7fb6a7dcca00c144476cccaeaae5042e99a285723b1a201e844ad67221bf5d428f1dc @@ -13399,66 +11066,6 @@ __metadata: languageName: node linkType: hard -"spdx-correct@npm:^3.0.0": - version: 3.2.0 - resolution: "spdx-correct@npm:3.2.0" - dependencies: - spdx-expression-parse: "npm:^3.0.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10/cc2e4dbef822f6d12142116557d63f5facf3300e92a6bd24e907e4865e17b7e1abd0ee6b67f305cae6790fc2194175a24dc394bfcc01eea84e2bdad728e9ae9a - languageName: node - linkType: hard - -"spdx-exceptions@npm:^2.1.0": - version: 2.3.0 - resolution: "spdx-exceptions@npm:2.3.0" - checksum: 10/cb69a26fa3b46305637123cd37c85f75610e8c477b6476fa7354eb67c08128d159f1d36715f19be6f9daf4b680337deb8c65acdcae7f2608ba51931540687ac0 - languageName: node - linkType: hard - -"spdx-expression-parse@npm:^3.0.0": - version: 3.0.1 - resolution: "spdx-expression-parse@npm:3.0.1" - dependencies: - spdx-exceptions: "npm:^2.1.0" - spdx-license-ids: "npm:^3.0.0" - checksum: 10/a1c6e104a2cbada7a593eaa9f430bd5e148ef5290d4c0409899855ce8b1c39652bcc88a725259491a82601159d6dc790bedefc9016c7472f7de8de7361f8ccde - languageName: node - linkType: hard - -"spdx-license-ids@npm:^3.0.0": - version: 3.0.13 - resolution: "spdx-license-ids@npm:3.0.13" - checksum: 10/6328c516e958ceee80362dc657a58cab01c7fdb4667a1a4c1a3e91d069983977f87971340ee857eb66f65079b5d8561e56dc91510802cd7bebaae7632a6aa7fa - languageName: node - linkType: hard - -"split@npm:0.3": - version: 0.3.3 - resolution: "split@npm:0.3.3" - dependencies: - through: "npm:2" - checksum: 10/41b397e9fedc984ee1b061780bf173ef72a4f99265ca9cbccd9765b8cc0729eeee6cdeaf70664eb3eb0823e8430db033e50a33050498d75569fc743c6964c84e - languageName: node - linkType: hard - -"spotify-uri@npm:~4.0.0": - version: 4.0.0 - resolution: "spotify-uri@npm:4.0.0" - checksum: 10/f3cb0c5f1c4d0ae02746bcab837704df75d7d86d6011bd5424e5a3829b26fa04a6aea7e7382e8b7e3c3f221cf0d87afbeffaacdd9892cdd7ae506a06285970ec - languageName: node - linkType: hard - -"spotify-url-info@npm:^3.2.6": - version: 3.2.6 - resolution: "spotify-url-info@npm:3.2.6" - dependencies: - himalaya: "npm:~1.1.0" - spotify-uri: "npm:~4.0.0" - checksum: 10/4101df615b63307478a41375696ea188402116ce6e49193e1b930ca4f8046fd1246c6ef3db9b9138580c31850636e290cbf9bb7dc6099148702820bb23215d2c - languageName: node - linkType: hard - "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -13505,15 +11112,6 @@ __metadata: languageName: node linkType: hard -"stream-combiner@npm:~0.0.4": - version: 0.0.4 - resolution: "stream-combiner@npm:0.0.4" - dependencies: - duplexer: "npm:~0.1.1" - checksum: 10/844b622cfe8b9de45a6007404f613b60aaf85200ab9862299066204242f89a7c8033b1c356c998aa6cfc630f6cd9eba119ec1c6dc1f93e245982be4a847aee7d - languageName: node - linkType: hard - "streamsearch@npm:^1.1.0": version: 1.1.0 resolution: "streamsearch@npm:1.1.0" @@ -13521,13 +11119,6 @@ __metadata: languageName: node linkType: hard -"string-argv@npm:^0.3.1": - version: 0.3.2 - resolution: "string-argv@npm:0.3.2" - checksum: 10/f9d3addf887026b4b5f997a271149e93bf71efc8692e7dc0816e8807f960b18bcb9787b45beedf0f97ff459575ee389af3f189d8b649834cac602f2e857e75af - languageName: node - linkType: hard - "string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -13555,17 +11146,6 @@ __metadata: languageName: node linkType: hard -"string.prototype.padend@npm:^3.0.0": - version: 3.1.4 - resolution: "string.prototype.padend@npm:3.1.4" - dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.1.4" - es-abstract: "npm:^1.20.4" - checksum: 10/0625316ab60227a95d996205888bc906012c028adba052ff5044caf1ce1b127c8df512a13b17d1059c7c0139e319e251b1cfc91a4c5ebaab9432f90079dd2ea9 - languageName: node - linkType: hard - "string.prototype.trim@npm:^1.2.7": version: 1.2.7 resolution: "string.prototype.trim@npm:1.2.7" @@ -13650,13 +11230,6 @@ __metadata: languageName: node linkType: hard -"strip-eof@npm:^1.0.0": - version: 1.0.0 - resolution: "strip-eof@npm:1.0.0" - checksum: 10/40bc8ddd7e072f8ba0c2d6d05267b4e0a4800898c3435b5fb5f5a21e6e47dfaff18467e7aa0d1844bb5d6274c3097246595841fbfeb317e541974ee992cac506 - languageName: node - linkType: hard - "strip-final-newline@npm:^2.0.0": version: 2.0.0 resolution: "strip-final-newline@npm:2.0.0" @@ -13687,23 +11260,6 @@ __metadata: languageName: node linkType: hard -"strip-outer@npm:^2.0.0": - version: 2.0.0 - resolution: "strip-outer@npm:2.0.0" - checksum: 10/14ef9fe861e59a5f1555f1860982ae4edce2edb4ed34ab1b37cb62a8ba2f7c3540cbca6c884eabe4006e6cd729ab5d708a631169dd5b66fda570836e7e3b6589 - languageName: node - linkType: hard - -"strtok3@npm:^6.2.4": - version: 6.3.0 - resolution: "strtok3@npm:6.3.0" - dependencies: - "@tokenizer/token": "npm:^0.3.0" - peek-readable: "npm:^4.1.0" - checksum: 10/98fba564d3830202aa3a6bcd5ccaf2cbd849bd87ae79ece91d337e1913916705a8e633c9577138d030a984f8ec987dea51807e01252f995cf5e183fdea35eb2b - languageName: node - linkType: hard - "strtok3@npm:^7.0.0-alpha.9": version: 7.0.0 resolution: "strtok3@npm:7.0.0" @@ -13774,15 +11330,6 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^5.3.0": - version: 5.5.0 - resolution: "supports-color@npm:5.5.0" - dependencies: - has-flag: "npm:^3.0.0" - checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -13919,13 +11466,6 @@ __metadata: languageName: node linkType: hard -"through@npm:2, through@npm:~2.3, through@npm:~2.3.1": - version: 2.3.8 - resolution: "through@npm:2.3.8" - checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 - languageName: node - linkType: hard - "tinybench@npm:^2.5.0": version: 2.5.1 resolution: "tinybench@npm:2.5.1" @@ -13954,24 +11494,6 @@ __metadata: languageName: node linkType: hard -"tldts-core@npm:^6.0.8": - version: 6.0.8 - resolution: "tldts-core@npm:6.0.8" - checksum: 10/86924f36a4db822cb9b97e4c23aae066600020e0aadb71ebeff1bfab2b72fd900ba5f811765d35e0fa8026d8feecb8b549ca0d911b2e9f4106f34d38eb30b7ab - languageName: node - linkType: hard - -"tldts@npm:^6.0.5": - version: 6.0.8 - resolution: "tldts@npm:6.0.8" - dependencies: - tldts-core: "npm:^6.0.8" - bin: - tldts: bin/cli.js - checksum: 10/a225836445da3134db89c78fe0a831b84bfbff11e65a70e36b9956dbcf9d0b06ec9aee856359d5fb5e0f8cb5bd647f04789fda6a919b36ea4892c5e934cd1782 - languageName: node - linkType: hard - "tmp@npm:^0.2.1": version: 0.2.3 resolution: "tmp@npm:0.2.3" @@ -13995,16 +11517,6 @@ __metadata: languageName: node linkType: hard -"token-types@npm:^4.1.1": - version: 4.2.1 - resolution: "token-types@npm:4.2.1" - dependencies: - "@tokenizer/token": "npm:^0.3.0" - ieee754: "npm:^1.2.1" - checksum: 10/2995257d246387e773758c3c92a3cc99d0c0bf13cbafe0de5d712e4c35ed298da6704e21545cb123fa1f1b42ad62936c35bbd0611018b735e78c30b8b22b42d9 - languageName: node - linkType: hard - "token-types@npm:^5.0.0-alpha.2": version: 5.0.0-alpha.2 resolution: "token-types@npm:5.0.0-alpha.2" @@ -14015,18 +11527,6 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^4.1.3": - version: 4.1.3 - resolution: "tough-cookie@npm:4.1.3" - dependencies: - psl: "npm:^1.1.33" - punycode: "npm:^2.1.1" - universalify: "npm:^0.2.0" - url-parse: "npm:^1.5.3" - checksum: 10/cf148c359b638a7069fc3ba9a5257bdc9616a6948a98736b92c3570b3f8401cf9237a42bf716878b656f372a1fb65b74dd13a46ccff8eceba14ffd053d33f72a - languageName: node - linkType: hard - "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -14059,15 +11559,6 @@ __metadata: languageName: node linkType: hard -"trim-repeated@npm:^2.0.0": - version: 2.0.0 - resolution: "trim-repeated@npm:2.0.0" - dependencies: - escape-string-regexp: "npm:^5.0.0" - checksum: 10/4086eb0bc560f3da0370f427f423db4e3fc0a8e1560ecffc3b68512071319fe82dc9dd86d76b981d36ada76d7d49c3f8897ac054c87bc177e7a25abfd29e2bcd - languageName: node - linkType: hard - "trough@npm:^2.0.0": version: 2.1.0 resolution: "trough@npm:2.1.0" @@ -14098,29 +11589,6 @@ __metadata: languageName: node linkType: hard -"ts-mixer@npm:^6.0.3": - version: 6.0.3 - resolution: "ts-mixer@npm:6.0.3" - checksum: 10/ac9178bdac5e5f760472269ad4c461587a0f6793532ddbef1326bb01482425a6247be98f9bd11bf35a9fdd36b63b8c8dde393942b9b9ee52d154eef082fca39a - languageName: node - linkType: hard - -"tsc-watch@npm:^6.0.0": - version: 6.0.4 - resolution: "tsc-watch@npm:6.0.4" - dependencies: - cross-spawn: "npm:^7.0.3" - node-cleanup: "npm:^2.1.2" - ps-tree: "npm:^1.2.0" - string-argv: "npm:^0.3.1" - peerDependencies: - typescript: "*" - bin: - tsc-watch: dist/lib/tsc-watch.js - checksum: 10/b0e793f63fbb260d4bb94f7fac6cb4b31c7bd710f16cac5018859b87a4550663d404a2cf8478105a48d77f7c72a9cf5568d605381eb6e44b77055e4f9031886a - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.14.1": version: 3.14.2 resolution: "tsconfig-paths@npm:3.14.2" @@ -14133,13 +11601,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.x, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.1, tslib@npm:^2.5.3": - version: 2.5.3 - resolution: "tslib@npm:2.5.3" - checksum: 10/d507e60ebe2480af4efc1655dfdb2762bb6ca57d76c4ba680375af801493648c2e97808bbd7e54691eb40e33a7e2e793cdef9c24ce6a8539b03cac8b26e09a61 - languageName: node - linkType: hard - "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -14147,6 +11608,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.5.3": + version: 2.5.3 + resolution: "tslib@npm:2.5.3" + checksum: 10/d507e60ebe2480af4efc1655dfdb2762bb6ca57d76c4ba680375af801493648c2e97808bbd7e54691eb40e33a7e2e793cdef9c24ce6a8539b03cac8b26e09a61 + languageName: node + linkType: hard + "tslib@npm:^2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" @@ -14215,23 +11683,6 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^3.12.7": - version: 3.12.7 - resolution: "tsx@npm:3.12.7" - dependencies: - "@esbuild-kit/cjs-loader": "npm:^2.4.2" - "@esbuild-kit/core-utils": "npm:^3.0.0" - "@esbuild-kit/esm-loader": "npm:^2.5.5" - fsevents: "npm:~2.3.2" - dependenciesMeta: - fsevents: - optional: true - bin: - tsx: dist/cli.js - checksum: 10/fc126d08a50eb85e09c11acb440cdbc549b1ed61e3d3603c83e45825ca5a9aeeb4bf12e5f6ba0996aa663218cd623bc703cc68b9399b91d7a8e8961bea017bb4 - languageName: node - linkType: hard - "turbo-darwin-64@npm:1.10.16": version: 1.10.16 resolution: "turbo-darwin-64@npm:1.10.16" @@ -14373,16 +11824,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.6.3": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/458f7220ab11e0fc191514cc41be1707645ec9a8c2d609448a448e18c522cef9646f58728f6811185a4c35613dacdf6c98cf8965c88b3541d0288c47291e4300 - languageName: node - linkType: hard - "typescript@npm:^5.2.2": version: 5.2.2 resolution: "typescript@npm:5.2.2" @@ -14413,16 +11854,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^4.6.3#optional!builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin::version=4.9.5&hash=289587" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/5659316360b5cc2d6f5931b346401fa534107b68b60179cf14970e27978f0936c1d5c46f4b5b8175f8cba0430f522b3ce355b4b724c0ea36ce6c0347fab25afd - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A^5.2.2#optional!builtin": version: 5.2.2 resolution: "typescript@patch:typescript@npm%3A5.2.2#optional!builtin::version=5.2.2&hash=f3b441" @@ -14469,33 +11900,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.22.1": - version: 5.22.1 - resolution: "undici@npm:5.22.1" - dependencies: - busboy: "npm:^1.6.0" - checksum: 10/4e4ae061372508bad6c017e0188cdbf1bb73e427d881aefe6277f88cb0bdd45b57bb88d7ab6fc136ff08e7d022bd83ca550a28272aebfb36b28c06fe8f07ac5e - languageName: node - linkType: hard - -"undici@npm:^5.23.0, undici@npm:^5.24.0": - version: 5.24.0 - resolution: "undici@npm:5.24.0" - dependencies: - busboy: "npm:^1.6.0" - checksum: 10/ae43417f36e7adf14bfae9a632352a9a53ce8ca6a3c6c5ff9e3561a17c5f999a1dfdd85072f270dfab75224a63b143780b2fc3e9a6000eb7878d6c67c894a4a1 - languageName: node - linkType: hard - -"undici@npm:^5.25.2": - version: 5.26.3 - resolution: "undici@npm:5.26.3" - dependencies: - "@fastify/busboy": "npm:^2.0.0" - checksum: 10/7280135e89c6f96f17f02fa99b8dcf5c64511d36de31b5cea0e1a858c8a16f07ea22aba524d5b8f574e9341a543d295aebb20cb715545829f2e959329149a638 - languageName: node - linkType: hard - "undici@npm:^5.8.0": version: 5.8.0 resolution: "undici@npm:5.8.0" @@ -14503,20 +11907,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^6.11.1": - version: 6.13.0 - resolution: "undici@npm:6.13.0" - checksum: 10/4ec2038e95779d4f1114a5dcf5bc74ec59c7fc76f6287f8a6bea6d69113f0190e6d41cc6e14409b5d912b0a92ce910b33bfa05808f40b6bf2b802b58b427f2cf - languageName: node - linkType: hard - -"unfetch@npm:^5.0.0": - version: 5.0.0 - resolution: "unfetch@npm:5.0.0" - checksum: 10/8a59f9d910f179ef588aa30885849de7b4c895a85b3679ab4da7305be3751b85a4811d9164d87960fef1a388b9a7afdc23ab2154f517db040b27171578fa9e8b - languageName: node - linkType: hard - "unherit@npm:^3.0.0": version: 3.0.1 resolution: "unherit@npm:3.0.1" @@ -14764,13 +12154,6 @@ __metadata: languageName: node linkType: hard -"universalify@npm:^0.2.0": - version: 0.2.0 - resolution: "universalify@npm:0.2.0" - checksum: 10/e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 - languageName: node - linkType: hard - "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -14815,16 +12198,6 @@ __metadata: languageName: node linkType: hard -"url-parse@npm:^1.5.3": - version: 1.5.10 - resolution: "url-parse@npm:1.5.10" - dependencies: - querystringify: "npm:^2.1.1" - requires-port: "npm:^1.0.0" - checksum: 10/c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad - languageName: node - linkType: hard - "use-callback-ref@npm:^1.3.0": version: 1.3.0 resolution: "use-callback-ref@npm:1.3.0" @@ -14891,16 +12264,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:^3.0.1": - version: 3.0.4 - resolution: "validate-npm-package-license@npm:3.0.4" - dependencies: - spdx-correct: "npm:^3.0.0" - spdx-expression-parse: "npm:^3.0.0" - checksum: 10/86242519b2538bb8aeb12330edebb61b4eb37fd35ef65220ab0b03a26c0592c1c8a7300d32da3cde5abd08d18d95e8dabfad684b5116336f6de9e6f207eec224 - languageName: node - linkType: hard - "vary@npm:^1, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -15138,13 +12501,6 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^3.0.3": - version: 3.2.1 - resolution: "web-streams-polyfill@npm:3.2.1" - checksum: 10/08fcf97b7883c1511dd3da794f50e9bde75a660884783baaddb2163643c21a94086f394dc4bd20dff0f55c98d98d60c4bea05a5809ef5005bdf835b63ada8900 - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -15239,17 +12595,6 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.9": - version: 1.3.1 - resolution: "which@npm:1.3.1" - dependencies: - isexe: "npm:^2.0.0" - bin: - which: ./bin/which - checksum: 10/549dcf1752f3ee7fbb64f5af2eead4b9a2f482108b7de3e85c781d6c26d8cf6a52d37cfbe0642a155fa6470483fe892661a859c03157f24c669cf115f3bbab5e - languageName: node - linkType: hard - "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -15273,7 +12618,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: @@ -15322,6 +12667,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.17.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/5e1dcb0ae70c6e2f158f5b446e0a72a2cd335b07aba73ee1872e9bae1285382286a10e53ed479db21bdd690a5dfd05641a768611ebb236253c62fefa43ef58b4 + languageName: node + linkType: hard + "ws@npm:^8.8.1": version: 8.8.1 resolution: "ws@npm:8.8.1" @@ -15376,13 +12736,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^2.1.2": - version: 2.1.2 - resolution: "yallist@npm:2.1.2" - checksum: 10/75fc7bee4821f52d1c6e6021b91b3e079276f1a9ce0ad58da3c76b79a7e47d6f276d35e206a96ac16c1cf48daee38a8bb3af0b1522a3d11c8ffe18f898828832 - languageName: node - linkType: hard - "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -15433,61 +12786,6 @@ __metadata: languageName: node linkType: hard -"youtube-dl-exec@npm:^2.1.11": - version: 2.1.11 - resolution: "youtube-dl-exec@npm:2.1.11" - dependencies: - dargs: "npm:~7.0.0" - execa: "npm:~5.1.0" - is-unix: "npm:~2.0.1" - simple-get: "npm:~4.0.1" - checksum: 10/96584b7980679bf578a209b6d240c2c4adda9909c53e2f66a146267c71c8c51a68828ba0066087defbb4d294303a62a996bbeefd94d01c3f1a4e0fb837870a88 - languageName: node - linkType: hard - -"youtube-ext@npm:^1.1.14": - version: 1.1.14 - resolution: "youtube-ext@npm:1.1.14" - dependencies: - axios: "npm:^1.5.0" - checksum: 10/6825baea5a222d444a7b7214ba121b80b244435816ca068dd551a595cb47a13e4ed92d4ee959e480847c1c6123fbfd4272ba9c6ce156808cb2499b9f2563ff22 - languageName: node - linkType: hard - -"youtube-ext@npm:^1.1.23": - version: 1.1.23 - resolution: "youtube-ext@npm:1.1.23" - dependencies: - undici: "npm:^6.11.1" - checksum: 10/3a8bb7612327d869914f895b22d6b0fcd00af857d38b0cc78a966cdf4d2ff386e7ff9c10b333baf43edb7ba486d657e2cb1cd6c07c5e623e9fc088778781e356 - languageName: node - linkType: hard - -"youtube-sr@npm:^4.3.9": - version: 4.3.9 - resolution: "youtube-sr@npm:4.3.9" - checksum: 10/ad0ba149c4013f917c54cabebb0244ffbcb1f777cc23f7a7a1bc71c6b937dc2434200c936274394b9d7a18ec18393d341ca2eaac24984a6b72a090957be130ea - languageName: node - linkType: hard - -"yt-stream@npm:^1.4.8": - version: 1.4.8 - resolution: "yt-stream@npm:1.4.8" - checksum: 10/18fe099e9037287cdab899ee3c9448f79ff2b86992ff1bb7fff6dc0525c00addba2ae02f3c8998bd53b86b087b637bd8421c84a23c6e6a9922805ddb1a9b5cac - languageName: node - linkType: hard - -"ytdl-core@npm:^4.11.4": - version: 4.11.4 - resolution: "ytdl-core@npm:4.11.4" - dependencies: - m3u8stream: "npm:^0.8.6" - miniget: "npm:^4.2.2" - sax: "npm:^1.1.3" - checksum: 10/ee455a446f7c5d1c5a64d888189c58a490178fa734af57b5de455ac18761a54127a7b8eaa44849293c2186ed92d2080f6c56dcc407a11ae25192486aaeceda83 - languageName: node - linkType: hard - "zod-to-json-schema@npm:^3.20.3": version: 3.23.0 resolution: "zod-to-json-schema@npm:3.23.0" From 92644bee91780f142b33bf209faa8af39ce144c2 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Mon, 17 Jun 2024 21:55:13 +0545 Subject: [PATCH 2/7] feat: adapter init --- .../src/{adapter.ts => Adapter.ts} | 11 +++-- packages/discord-player/src/Player.ts | 13 ++--- packages/discord-player/src/context.ts | 17 ------- .../discord-player/src/core/GuildQueue.ts | 0 .../src/structures/ArrayList.ts | 48 +++++++++++++++++++ .../discord-player/src/structures/Queue.ts | 15 ++++++ .../discord-player/src/structures/Stack.ts | 15 ++++++ 7 files changed, 89 insertions(+), 30 deletions(-) rename packages/discord-player/src/{adapter.ts => Adapter.ts} (87%) delete mode 100644 packages/discord-player/src/context.ts create mode 100644 packages/discord-player/src/core/GuildQueue.ts create mode 100644 packages/discord-player/src/structures/ArrayList.ts create mode 100644 packages/discord-player/src/structures/Queue.ts create mode 100644 packages/discord-player/src/structures/Stack.ts diff --git a/packages/discord-player/src/adapter.ts b/packages/discord-player/src/Adapter.ts similarity index 87% rename from packages/discord-player/src/adapter.ts rename to packages/discord-player/src/Adapter.ts index fcd2cda2e7..03fb8711eb 100644 --- a/packages/discord-player/src/adapter.ts +++ b/packages/discord-player/src/Adapter.ts @@ -1,6 +1,5 @@ import { unsafe } from './common/types'; -import { getPlayerAdapterContext } from './context'; -import { Player } from './player'; +import { Player } from './Player'; export type OnGatewayPacket = (packet: unsafe) => void; @@ -38,9 +37,11 @@ export interface AdapterImpl { } export class Adapter implements AdapterImpl { - public readonly player: Player; - public constructor(private readonly config: IAdapter) { - this.player = getPlayerAdapterContext(); + private player!: Player; + public constructor(private readonly config: IAdapter) {} + + public setPlayer(player: Player): void { + this.player = player; } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/discord-player/src/Player.ts b/packages/discord-player/src/Player.ts index 2ac871dae1..a5a83d383c 100644 --- a/packages/discord-player/src/Player.ts +++ b/packages/discord-player/src/Player.ts @@ -1,15 +1,12 @@ -import type { IAdapter } from './adapter'; -import { setPlayerAdapterContext } from './context'; +import type { Adapter } from './Adapter'; export class Player { - public readonly adapter: IAdapter; - public constructor(adapter: PlayerAdapterInterface) { - this.adapter = setPlayerAdapterContext(this, adapter); + public constructor(public readonly adapter: Adapter) { + this.adapter = adapter; + this.adapter.setPlayer(this); } } -export type PlayerAdapterInterface = () => IAdapter; - -export function createPlayer(adapter: PlayerAdapterInterface>) { +export function createPlayer(adapter: Adapter) { return new Player(adapter); } diff --git a/packages/discord-player/src/context.ts b/packages/discord-player/src/context.ts deleted file mode 100644 index 897f1bc8eb..0000000000 --- a/packages/discord-player/src/context.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AsyncLocalStorage } from 'node:async_hooks'; -import type { Player, PlayerAdapterInterface } from './player'; -import { unsafe } from './common/types'; - -const PlayerAdapterContext = new AsyncLocalStorage>(); - -export function getPlayerAdapterContext(): Player { - const ctx = PlayerAdapterContext.getStore(); - - if (!ctx) throw new Error('No player adapter context found'); - - return ctx; -} - -export function setPlayerAdapterContext(player: Player, adapterInterface: PlayerAdapterInterface) { - return PlayerAdapterContext.run(player, adapterInterface); -} diff --git a/packages/discord-player/src/core/GuildQueue.ts b/packages/discord-player/src/core/GuildQueue.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/discord-player/src/structures/ArrayList.ts b/packages/discord-player/src/structures/ArrayList.ts new file mode 100644 index 0000000000..ef24d0fa40 --- /dev/null +++ b/packages/discord-player/src/structures/ArrayList.ts @@ -0,0 +1,48 @@ +import { randomInt } from 'crypto'; + +export class ArrayList extends Array { + private _prev: T[] = []; + + public random() { + return this[randomInt(this.length)]; + } + + public flush() { + this._prev = []; + } + + public canUnshuffle() { + return this._prev.length > 0; + } + + public shuffle(inPlace = true) { + const len = this.length; + + if (inPlace) { + this._prev = this.slice(); + } else { + this._prev = []; + } + + const list = inPlace ? this : this.slice(); + + for (let i = 0; i < len; i++) { + const j = Math.floor(Math.random() * len); + [list[i], list[j]] = [list[j], list[i]]; + } + } + + public unshuffle() { + if (!this._prev.length) return false; + + this.splice(0, this.length, ...this._prev); + this._prev = []; + + return true; + } + + public clear(): void { + this.flush(); + this.length = 0; + } +} diff --git a/packages/discord-player/src/structures/Queue.ts b/packages/discord-player/src/structures/Queue.ts new file mode 100644 index 0000000000..f689040e70 --- /dev/null +++ b/packages/discord-player/src/structures/Queue.ts @@ -0,0 +1,15 @@ +import { ArrayList } from './ArrayList'; + +export class Queue extends ArrayList { + public add(...items: T[]): void { + this.push(...items); + } + + public next(): T | undefined { + return this.shift(); + } + + public peek(): T | undefined { + return this[0]; + } +} diff --git a/packages/discord-player/src/structures/Stack.ts b/packages/discord-player/src/structures/Stack.ts new file mode 100644 index 0000000000..721c08c627 --- /dev/null +++ b/packages/discord-player/src/structures/Stack.ts @@ -0,0 +1,15 @@ +import { ArrayList } from './ArrayList'; + +export class Stack extends ArrayList { + public add(...items: T[]): void { + this.unshift(...items); + } + + public next(): T | undefined { + return this.shift(); + } + + public peek(): T | undefined { + return this[0]; + } +} From 7cb7c90bafebc28ec7bf2aa56f600bbdcc91ca0f Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sat, 29 Jun 2024 20:35:59 +0545 Subject: [PATCH 3/7] feat: add more features --- .eslintrc.json | 12 +- packages/discord-player/README.md | 168 +---- packages/discord-player/package.json | 1 + packages/discord-player/src/Adapter.ts | 64 +- packages/discord-player/src/Player.ts | 89 ++- packages/discord-player/src/common/types.ts | 7 + .../discord-player/src/core/GuildQueue.ts | 105 +++ packages/discord-player/src/core/Plugin.ts | 75 ++ .../src/core/manager/GuildQueueManager.ts | 67 ++ .../src/core/manager/PlayerNodeManager.ts | 79 ++ .../src/core/manager/PluginsManager.ts | 34 + .../src/structures/ArrayList.ts | 29 + .../src/utils/DependencyReport.ts | 165 +++++ .../discord-player/src/voip/connection.ts | 0 packages/discord-player/src/voip/constants.ts | 29 - packages/discord-player/src/voip/libsodium.ts | 80 -- .../discord-player/src/voip/networking.ts | 65 -- packages/discord-player/src/voip/udp.ts | 134 ---- packages/discord-player/src/voip/websocket.ts | 117 --- packages/discord-player/tsconfig.json | 4 +- packages/discord-voip/README.md | 6 + packages/discord-voip/package.json | 41 ++ packages/discord-voip/src/DataStore.ts | 192 +++++ packages/discord-voip/src/VoiceConnection.ts | 689 ++++++++++++++++++ .../discord-voip/src/audio/AudioPlayer.ts | 637 ++++++++++++++++ .../src/audio/AudioPlayerError.ts | 22 + .../discord-voip/src/audio/AudioResource.ts | 272 +++++++ .../src/audio/PlayerSubscription.ts | 36 + .../src/audio/TransformerGraph.ts | 271 +++++++ packages/discord-voip/src/audio/index.ts | 23 + packages/discord-voip/src/common/types.ts | 2 + packages/discord-voip/src/index.ts | 23 + packages/discord-voip/src/joinVoiceChannel.ts | 68 ++ .../discord-voip/src/networking/Networking.ts | 589 +++++++++++++++ .../src/networking/VoiceUDPSocket.ts | 180 +++++ .../src/networking/VoiceWebSocket.ts | 180 +++++ packages/discord-voip/src/networking/index.ts | 6 + packages/discord-voip/src/util/Secretbox.ts | 85 +++ packages/discord-voip/src/util/abortAfter.ts | 14 + packages/discord-voip/src/util/adapter.ts | 54 ++ packages/discord-voip/src/util/demuxProbe.ts | 128 ++++ packages/discord-voip/src/util/entersState.ts | 45 ++ packages/discord-voip/src/util/index.ts | 6 + packages/discord-voip/src/util/util.ts | 4 + packages/ffmpeg/src/FFmpeg.ts | 257 +++---- packages/ffmpeg/src/index.ts | 23 + packages/node/LICENSE | 21 + packages/node/README.md | 15 + packages/node/__test__/sum.spec.ts | 8 + packages/node/package.json | 42 ++ packages/node/src/PlayerNode.ts | 178 +++++ packages/node/src/index.ts | 4 + packages/node/tsconfig.json | 6 + packages/node/tsup.config.ts | 6 + packages/node/vitest.config.ts | 9 + packages/opus/src/OggDemuxer.ts | 134 ++++ packages/opus/src/OpusEncoder.ts | 379 ++++++++++ packages/opus/src/WebmBase.ts | 220 ++++++ packages/opus/src/WebmDemuxer.ts | 22 + packages/opus/src/index.ts | 354 +-------- yarn.lock | 482 ++++++++++++ 61 files changed, 5929 insertions(+), 1128 deletions(-) create mode 100644 packages/discord-player/src/core/Plugin.ts create mode 100644 packages/discord-player/src/core/manager/GuildQueueManager.ts create mode 100644 packages/discord-player/src/core/manager/PlayerNodeManager.ts create mode 100644 packages/discord-player/src/core/manager/PluginsManager.ts create mode 100644 packages/discord-player/src/utils/DependencyReport.ts delete mode 100644 packages/discord-player/src/voip/connection.ts delete mode 100644 packages/discord-player/src/voip/constants.ts delete mode 100644 packages/discord-player/src/voip/libsodium.ts delete mode 100644 packages/discord-player/src/voip/networking.ts delete mode 100644 packages/discord-player/src/voip/udp.ts delete mode 100644 packages/discord-player/src/voip/websocket.ts create mode 100644 packages/discord-voip/README.md create mode 100644 packages/discord-voip/package.json create mode 100644 packages/discord-voip/src/DataStore.ts create mode 100644 packages/discord-voip/src/VoiceConnection.ts create mode 100644 packages/discord-voip/src/audio/AudioPlayer.ts create mode 100644 packages/discord-voip/src/audio/AudioPlayerError.ts create mode 100644 packages/discord-voip/src/audio/AudioResource.ts create mode 100644 packages/discord-voip/src/audio/PlayerSubscription.ts create mode 100644 packages/discord-voip/src/audio/TransformerGraph.ts create mode 100644 packages/discord-voip/src/audio/index.ts create mode 100644 packages/discord-voip/src/common/types.ts create mode 100644 packages/discord-voip/src/index.ts create mode 100644 packages/discord-voip/src/joinVoiceChannel.ts create mode 100644 packages/discord-voip/src/networking/Networking.ts create mode 100644 packages/discord-voip/src/networking/VoiceUDPSocket.ts create mode 100644 packages/discord-voip/src/networking/VoiceWebSocket.ts create mode 100644 packages/discord-voip/src/networking/index.ts create mode 100644 packages/discord-voip/src/util/Secretbox.ts create mode 100644 packages/discord-voip/src/util/abortAfter.ts create mode 100644 packages/discord-voip/src/util/adapter.ts create mode 100644 packages/discord-voip/src/util/demuxProbe.ts create mode 100644 packages/discord-voip/src/util/entersState.ts create mode 100644 packages/discord-voip/src/util/index.ts create mode 100644 packages/discord-voip/src/util/util.ts create mode 100644 packages/node/LICENSE create mode 100644 packages/node/README.md create mode 100644 packages/node/__test__/sum.spec.ts create mode 100644 packages/node/package.json create mode 100644 packages/node/src/PlayerNode.ts create mode 100644 packages/node/src/index.ts create mode 100644 packages/node/tsconfig.json create mode 100644 packages/node/tsup.config.ts create mode 100644 packages/node/vitest.config.ts create mode 100644 packages/opus/src/OggDemuxer.ts create mode 100644 packages/opus/src/OpusEncoder.ts create mode 100644 packages/opus/src/WebmBase.ts create mode 100644 packages/opus/src/WebmDemuxer.ts diff --git a/.eslintrc.json b/.eslintrc.json index e172b02c9f..c4129361c4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,21 +4,17 @@ "env": { "node": true }, - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "rules": { "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-unsafe-declaration-merging": "off", "no-async-promise-executor": "off", "semi": "error", "no-console": "error" } -} \ No newline at end of file +} diff --git a/packages/discord-player/README.md b/packages/discord-player/README.md index d3c2cea431..c678d31003 100644 --- a/packages/discord-player/README.md +++ b/packages/discord-player/README.md @@ -1,169 +1,3 @@ # Discord Player -Discord Player is a robust framework for developing Discord Music bots using JavaScript and TypeScript. It is built on top of the [discord-voip](https://npm.im/discord-voip) library and offers a comprehensive set of customizable tools, making it one of the most feature enrich framework in town. - -[![downloadsBadge](https://img.shields.io/npm/dt/discord-player?style=for-the-badge)](https://npmjs.com/discord-player) -[![versionBadge](https://img.shields.io/npm/v/discord-player?style=for-the-badge)](https://npmjs.com/discord-player) -[![discordBadge](https://img.shields.io/discord/558328638911545423?style=for-the-badge&color=7289da)](https://androz2091.fr/discord) - -# Why Choose Discord Player? - -- Beginner-friendly with easy-to-understand features -- TypeScript support -- Offers hackable APIs. -- Supports audio player sharing -- Quick and easy setup process -- Wide range of player management features -- Offers 64+ built-in audio filter presets -- Highly customizable according to your needs -- Automatic queue management -- Query caching support -- Extensible sources through the Extractors API -- Object-oriented design -- Built-in stats tracker -- Offers easy debugging methods -- Out-of-the-box voice states handling -- IP Rotation support -- Easy serialization and deserialization - -## Installation - -### Before you start - -Discord Player requires Discord.js 14.0 or higher. Please ensure that you have a compatible version by running `npm list discord.js` in your terminal. If you're using an earlier version, please update it. The [discord.js Guide](https://discordjs.guide) provides resources to assist you with the update process. - -#### Main Library - -```bash -$ npm install --save discord-player # main library -$ npm install --save @discord-player/extractor # extractors provider -``` - -> Discord Player recognizes `@discord-player/extractor` and loads it automatically by default. Just invoke `await player.extractors.loadDefault()`. - -#### Opus Library - -Since Discord only accepts opus packets, you need to install the opus library. Discord Player supports multiple opus libraries, such as: - -- [mediaplex](https://npmjs.com/mediaplex) -- [@discordjs/opus](https://npmjs.com/@discordjs/opus) -- [opusscript](https://npmjs.com/opusscript) -- [@evan/opus](https://npmjs.com/@evan/opus) -- [node-opus](https://npmjs.com/node-opus) - -Among these, mediaplex is the recommended library as it adds more functionalities to discord-player than just libopus interface. You can install opus libraries by running: - -```bash -$ npm install --save mediaplex -# or -$ npm install --save @discordjs/opus -# or -$ npm install --save opusscript -# or -$ npm install --save @evan/opus -# or -$ npm install --save node-opus -``` - -#### FFmpeg or Avconv - -FFmpeg or Avconv is required for media transcoding. You can obtain it from [https://ffmpeg.org](https://ffmpeg.org) or via npm. - -> We do not recommend installing ffmpeg via npm because binaries pulled from npm is known to be unstable. It is recommended to install it from the official source. - -```bash -$ npm install --save ffmpeg-static -# or -$ npm install --save @ffmpeg-installer/ffmpeg -# or -$ npm install --save @node-ffmpeg/node-ffmpeg-installer -# or -$ npm install --save ffmpeg-binaries -``` - -> Use `FFMPEG_PATH` environment variable to load ffmpeg from custom path. - -#### Streaming Library - -YouTube streaming is not supported without installing one of the following package. If you want to add support for YouTube playback, you need to install a streaming library. This step is not needed if you do not plan on using youtube source. - -```bash -$ npm install --save youtube-ext -# or -$ npm install --save play-dl -# or -$ npm install --save @distube/ytdl-core -# or -$ npm install --save yt-stream -# or -$ npm install --save ytdl-core -``` - -We recommend using `youtube-ext` for better performance. - -Once you have completed these installations, let's proceed with writing a simple music bot. - -### Setup - -Let's create a main player instance. This instance handles and keeps track of all the queues and its components. - -```js index.js -const { Player } = require('discord-player'); - -const client = new Discord.Client({ - // Make sure you have 'GuildVoiceStates' intent enabled - intents: ['GuildVoiceStates' /* Other intents */] -}); - -// this is the entrypoint for discord-player based application -const player = new Player(client); - -// Now, lets load all the default extractors, except 'YouTubeExtractor'. You can remove the filter if you want to include youtube. -await player.extractors.loadDefault((ext) => ext !== 'YouTubeExtractor'); -``` - -Discord Player is mostly events based. It emits different events based on the context and actions. Let's add a basic event listener to notify the user when a track starts to play: - -```js index.js -// this event is emitted whenever discord-player starts to play a track -player.events.on('playerStart', (queue, track) => { - // we will later define queue.metadata object while creating the queue - queue.metadata.channel.send(`Started playing **${track.cleanTitle}**!`); -}); -``` - -Let's move on to the command part. You can define the command as per your requirements. We will only focus on the command part: - -```js play.js -const { useMainPlayer } = require('discord-player'); - -export async function execute(interaction) { - const player = useMainPlayer(); - const channel = interaction.member.voice.channel; - if (!channel) return interaction.reply('You are not connected to a voice channel!'); // make sure we have a voice channel - const query = interaction.options.getString('query', true); // we need input/query to play - - // let's defer the interaction as things can take time to process - await interaction.deferReply(); - - try { - const { track } = await player.play(channel, query, { - nodeOptions: { - // nodeOptions are the options for guild node (aka your queue in simple word) - metadata: interaction // we can access this metadata object using queue.metadata later on - } - }); - - return interaction.followUp(`**${track.cleanTitle}** enqueued!`); - } catch (e) { - // let's return error if something failed - return interaction.followUp(`Something went wrong: ${e}`); - } -} -``` - -That's all it takes to build your own music bot. Please check out the [Documentation](https://discord-player.js.org) for more features/functionalities. - -## Community Resources - -Explore a curated list of resources built by the Discord Player community, including open-source music bots and extractors. Visit [https://discord-player.js.org/showcase](https://discord-player.js.org/showcase) for more information. +This branch is a work in progress of discord-player V7. diff --git a/packages/discord-player/package.json b/packages/discord-player/package.json index cdb06d2fce..ca16e6f4b5 100644 --- a/packages/discord-player/package.json +++ b/packages/discord-player/package.json @@ -52,6 +52,7 @@ "dependencies": { "@discord-player/equalizer": "workspace:^", "@discord-player/ffmpeg": "workspace:^", + "@discord-player/node": "workspace:^", "@discord-player/utils": "workspace:^", "@web-scrobbler/metadata-filter": "^3.1.0", "discord-voip": "^0.1.3", diff --git a/packages/discord-player/src/Adapter.ts b/packages/discord-player/src/Adapter.ts index 03fb8711eb..94914b1405 100644 --- a/packages/discord-player/src/Adapter.ts +++ b/packages/discord-player/src/Adapter.ts @@ -1,21 +1,8 @@ import { unsafe } from './common/types'; -import { Player } from './Player'; +import type { Player } from './Player'; export type OnGatewayPacket = (packet: unsafe) => void; -export interface IAdapter { - metadata: T; - sendPacket: (packet: unsafe) => void; - resolveGuild(guild: string): string; - resolveGuildByChannel(channel: string): string; - resolveChannel(channel: string): string; - resolveUser(user: string): string; - setRequestToSpeak(guild: string, channel: string, value: boolean): void; - isVoiceChannel(guild: string, channel: string): boolean; - isStageChannel(guild: string, channel: string): boolean; - getVoiceChannelMembersCount(guild: string, channel: string): number; -} - export interface IVoiceStateUpdateData { guild: string; channel: string; @@ -37,24 +24,57 @@ export interface AdapterImpl { } export class Adapter implements AdapterImpl { - private player!: Player; - public constructor(private readonly config: IAdapter) {} + protected player!: Player; - public setPlayer(player: Player): void { + public constructor(protected readonly metadata: T) {} + + public setPlayer(player: Player): void { this.player = player; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + /* eslint-disable @typescript-eslint/no-unused-vars */ public onPacket(packet: unsafe): void { throw new Error('Not implemented'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars public handleVoiceStateUpdate(oldState: IVoiceStateUpdateData, newState: IVoiceStateUpdateData): void { throw new Error('Not implemented'); } -} -export function createAdapter(adapterConfig: IAdapter): Adapter { - return new Adapter(adapterConfig); + public getVoiceChannelMembersCount(guild: string, channel: string): number { + throw new Error('Not implemented'); + } + + public isStageChannel(guild: string, channel: string): boolean { + throw new Error('Not implemented'); + } + + public isVoiceChannel(guild: string, channel: string): boolean { + throw new Error('Not implemented'); + } + + public resolveChannel(channel: string): string { + throw new Error('Not implemented'); + } + + public resolveGuild(guild: string): string { + throw new Error('Not implemented'); + } + + public resolveGuildByChannel(channel: string): string { + throw new Error('Not implemented'); + } + + public resolveUser(user: string): string { + throw new Error('Not implemented'); + } + + public setRequestToSpeak(guild: string, channel: string, value: boolean): void { + throw new Error('Not implemented'); + } + + public sendPacket(packet: unsafe): void { + throw new Error('Not implemented'); + } + /* eslint-enable @typescript-eslint/no-unused-vars */ } diff --git a/packages/discord-player/src/Player.ts b/packages/discord-player/src/Player.ts index a5a83d383c..be7e2a4203 100644 --- a/packages/discord-player/src/Player.ts +++ b/packages/discord-player/src/Player.ts @@ -1,12 +1,93 @@ +import type { PlayerNodeLike } from '@discord-player/node'; import type { Adapter } from './Adapter'; +import type { unsafe } from './common/types'; +import { PlayerNodeManager } from './core/manager/PlayerNodeManager'; +import { GuildQueueManager } from './core/manager/GuildQueueManager'; +import { PluginsManager } from './core/manager/PluginsManager'; + +/** + * Represents the options for the player manager. + */ +export interface PlayerOptions { + /** + * Whether to expose local player node (if available) to the network via the given port. Defaults to `false`. + */ + exposeNode?: number | false; +} + +export interface PlayerInitOptions extends PlayerOptions { + /** + * The remote nodes to use, if any. If not provided, the player will create a node locally. Defaults to `[]`. + */ + nodes?: PlayerNodeLike[]; +} + +/** + * Represents a player manager. + */ +export class Player { + /** + * The options for this player manager. + */ + public readonly options: PlayerOptions; + /** + * The player node manager. + */ + public readonly nodes: PlayerNodeManager; + /** + * The guild queue manager. + */ + public readonly queue: GuildQueueManager; + + /** + * The plugins manager. + */ + public readonly plugins: PluginsManager; + + /** + * Creates a new player manager. + * @param adapter The adapter to use + * @param options The options to use + */ + public constructor(public readonly adapter: Adapter, options: PlayerInitOptions = {}) { + const { nodes, ...rest } = options; -export class Player { - public constructor(public readonly adapter: Adapter) { this.adapter = adapter; this.adapter.setPlayer(this); + this.options = rest; + this.nodes = new PlayerNodeManager(this); + + if (nodes?.length) { + for (const node of nodes) { + this.nodes.create(node); + } + } + + this.queue = new GuildQueueManager(this); + this.plugins = new PluginsManager(this); + } + + /** + * Connects to all player nodes. + */ + public connect() { + return this.nodes.connectAll(); + } + + /** + * Disconnects from all player nodes. + */ + public disconnect() { + return this.nodes.deleteAll(); } } -export function createPlayer(adapter: Adapter) { - return new Player(adapter); +/** + * Creates a new player manager instance. + * @param adapter The adapter to use + * @param options The options to use + * @returns The created player manager + */ +export function createPlayer(adapter: Adapter, options: PlayerOptions) { + return new Player(adapter, options); } diff --git a/packages/discord-player/src/common/types.ts b/packages/discord-player/src/common/types.ts index d29b6e7ae7..28f7ae810d 100644 --- a/packages/discord-player/src/common/types.ts +++ b/packages/discord-player/src/common/types.ts @@ -1,2 +1,9 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type unsafe = any; + +type FN = (...args: unsafe[]) => unsafe; +type IgnoreList = FN | Array | Map | Set | WeakMap | WeakSet; + +export type DeepPartial = { + [P in keyof T]?: T[P] extends IgnoreList ? T[P] : DeepPartial | undefined; +}; diff --git a/packages/discord-player/src/core/GuildQueue.ts b/packages/discord-player/src/core/GuildQueue.ts index e69de29bb2..efdf95a37a 100644 --- a/packages/discord-player/src/core/GuildQueue.ts +++ b/packages/discord-player/src/core/GuildQueue.ts @@ -0,0 +1,105 @@ +import { unsafe } from '../common/types'; +import { Player } from '../Player'; +import { ArrayListLimitOptions } from '../structures/ArrayList'; +import { Queue } from '../structures/Queue'; +import { Stack } from '../structures/Stack'; +import type { Plugin } from './Plugin'; + +export interface GuildQueueAudioEffects { + /** + * The audio volume for this queue. + */ + volume: number; +} + +export interface GuildQueuePluginConfiguration { + /** + * The plugins allowed to be used in this queue. + */ + allowed: string[]; + /** + * The plugins disallowed to be used in this queue. + */ + disallowed: string[]; + /** + * Whether to expose the queue to the plugins. + */ + exposeQueue: boolean; + /** + * Validate the given plugin to either allow or disallow it. This method is an alternative to the `allowed` and `disallowed` arrays. + * If the plugin is not specified in the `allowed` or `disallowed` arrays, this method will be called. + * @param plugin The plugin to validate + */ + validate(plugin: Plugin): Promise; +} + +export interface GuildQueueOptions { + /** + * The guild ID. + */ + guild: string; + + /** + * The metadata of the queue. + */ + metadata: unsafe; + + /** + * The default audio effects of the queue. + */ + effects: GuildQueueAudioEffects; + + /** + * The history options. + */ + history: Omit; + + /** + * The queue options. + */ + queue: Omit; + + /** + * The plugins configuration for this queue. + */ + plugins: GuildQueuePluginConfiguration; +} + +export class GuildQueue { + /** + * The queue of tracks associated with this guild. + */ + public readonly tracks: Queue; + + /** + * The history of previous tracks associated with this guild. + */ + public readonly history: Stack; + + /** + * Creates a new guild queue. + * @param player The player to create the queue for + * @param options The options to use + */ + public constructor(public readonly player: Player, private readonly options: GuildQueueOptions) { + if (!this.options.guild) { + throw new Error('Expected key "GuildQueueOptions.guild" to be a string.'); + } + + this.tracks = new Queue({ + name: 'TracksQueue', + maxSize: this.options.queue.maxSize, + throwOnFull: this.options.queue.throwOnFull + }); + + this.history = new Stack({ + name: 'QueueHistory', + maxSize: this.options.history.maxSize, + throwOnFull: this.options.history.throwOnFull + }); + } + + public get id() { + return this.options.guild; + } +} diff --git a/packages/discord-player/src/core/Plugin.ts b/packages/discord-player/src/core/Plugin.ts new file mode 100644 index 0000000000..a7ca8a099d --- /dev/null +++ b/packages/discord-player/src/core/Plugin.ts @@ -0,0 +1,75 @@ +import { StreamType } from 'discord-voip'; +import { Readable } from 'stream'; +import { unsafe } from '../common/types'; + +export const PluginType = { + /** + * A plugin that provides metadata information for the given track source. + */ + MetadataExtractor: 'MetadataExtractor', + + /** + * A plugin that provides a stream for the given track. + */ + StreamProvider: 'StreamProvider', + + /** + * A plugin that provides audio effects for the given track stream. + */ + AudioEffect: 'AudioEffect' +} as const; + +export type PluginType = (typeof PluginType)[keyof typeof PluginType]; + +export interface StreamResult { + /** + * The stream or string provided by this plugin. + */ + stream: T; + /** + * The type of the stream. + */ + type: StreamType; +} + +export abstract class Plugin { + /** + * The name of this plugin. + */ + public abstract readonly name: string; + + /** + * Types of this plugin. + */ + public abstract readonly types: PluginType[]; + + /** + * Called when this plugin is registered. + */ + public abstract activate(): Promise; + + /** + * Called when this plugin is unregistered. + */ + public abstract deactivate(): Promise; + + /** + * Validate the given query. This method is only available to `MetadataExtractor` and `StreamProvider` plugins. + */ + public abstract validateQuery(query: string): Promise; + + /** + * Extract metadata from the given query. This method is only available to `MetadataExtractor` plugins. + */ + public abstract extractMetadata(query: string): Promise; + + /** + * Process the given source stream and return a new stream. This method is only available to `AudioEffect` plugins. + */ + public abstract applyEffects(source: Readable): Readable; + + /** + * Create a readable stream or a string from the given source. This method is only available to `StreamProvider` plugins. + */ + public abstract createStream(track: unsafe): StreamResult; +} diff --git a/packages/discord-player/src/core/manager/GuildQueueManager.ts b/packages/discord-player/src/core/manager/GuildQueueManager.ts new file mode 100644 index 0000000000..9b1e67f4df --- /dev/null +++ b/packages/discord-player/src/core/manager/GuildQueueManager.ts @@ -0,0 +1,67 @@ +import { Collection } from '@discord-player/utils'; +import type { Player } from '../../Player'; +import { GuildQueue, GuildQueueAudioEffects, type GuildQueueOptions } from '../GuildQueue'; +import type { DeepPartial } from '../../common/types'; + +export class GuildQueueManager { + /** + * The store of guild queues. + */ + public store: Collection = new Collection(); + + /** + * Creates a new guild queue manager. + * @param player The player to create the manager for + */ + public constructor(public readonly player: Player) {} + + /** + * Creates a new guild queue. + * @param guild The guild to create the queue for + */ + public create(guild: string, options: DeepPartial>) { + const effects: GuildQueueAudioEffects = { + volume: options.effects?.volume ?? 50 + }; + + const queue = new GuildQueue(this.player, { + guild, + metadata: options.metadata, + effects, + history: { + maxSize: options.history?.maxSize ?? 0, + throwOnFull: options.history?.throwOnFull ?? false + }, + queue: { + maxSize: options.queue?.maxSize ?? 0, + throwOnFull: options.queue?.throwOnFull ?? false + }, + plugins: { + allowed: options.plugins?.allowed ?? [], + disallowed: options.plugins?.disallowed ?? [], + exposeQueue: options.plugins?.exposeQueue ?? false, + validate: + options.plugins?.validate ?? + (() => { + return Promise.resolve(true); + }) + } + }); + + this.store.set(guild, queue); + + return queue; + } + + /** + * Deletes the given guild queue. + * @param guild The guild of the queue to delete + */ + public delete(guild: string) { + const queue = this.store.get(guild); + + if (!queue) return; + + this.store.delete(guild); + } +} diff --git a/packages/discord-player/src/core/manager/PlayerNodeManager.ts b/packages/discord-player/src/core/manager/PlayerNodeManager.ts new file mode 100644 index 0000000000..c4bec52568 --- /dev/null +++ b/packages/discord-player/src/core/manager/PlayerNodeManager.ts @@ -0,0 +1,79 @@ +import { PlayerNode, PlayerNodeLike } from '@discord-player/node'; +import { Collection } from '@discord-player/utils'; +import type { Player } from '../../Player'; + +export class PlayerNodeManager { + /** + * The store of player nodes. + */ + public store: Collection = new Collection(); + + /** + * Creates a new player node manager. + */ + public constructor(public readonly player: Player) {} + + /** + * Creates a new player node. + * @param node The node to create + */ + public create(node: PlayerNodeLike) { + const init = new PlayerNode(node, { + send: (packet) => { + return this.player.adapter.sendPacket(packet); + } + }); + + this.store.set(init.id, init); + + return node; + } + + /** + * Deletes the given player node. + * @param id The id of the player node to delete + */ + public async delete(id: string) { + const node = this.store.get(id); + + if (!node) return; + + await node.delete(); + + this.store.delete(id); + } + + /** + * Deletes all player nodes. + */ + public async deleteAll() { + await Promise.all(this.store.map(async (node) => node.delete())); + + this.store.clear(); + } + + /** + * Connects all player nodes. + */ + public async connectAll() { + if (!this.store.size) { + throw new Error('No player nodes to connect to.'); + } + + const nodes = await Promise.all( + this.store.map(async (node) => { + await node.connect(); + return node; + }) + ); + return nodes; + } + + /** + * Finds optimal player node. + */ + public getOptimalNode() { + // TODO: actually find the optimal node + return this.store.random() ?? null; + } +} diff --git a/packages/discord-player/src/core/manager/PluginsManager.ts b/packages/discord-player/src/core/manager/PluginsManager.ts new file mode 100644 index 0000000000..ab4dd4d3bd --- /dev/null +++ b/packages/discord-player/src/core/manager/PluginsManager.ts @@ -0,0 +1,34 @@ +import { Collection } from '@discord-player/utils'; +import type { Player } from '../../Player'; +import { Plugin } from '../Plugin'; + +export class PluginsManager { + /** + * The store of guild queues. + */ + public store: Collection = new Collection(); + + /** + * Creates a new guild queue manager. + * @param player The player to create the manager for + */ + public constructor(public readonly player: Player) {} + + /** + * Loads a plugin. + * @param plugin The plugin to load + */ + public async load(plugin: Plugin) { + await plugin.activate(); + this.store.set(plugin.name, plugin); + } + + /** + * Unloads a plugin. + * @param plugin The plugin to unload + */ + public async unload(plugin: Plugin) { + await plugin.deactivate(); + this.store.delete(plugin.name); + } +} diff --git a/packages/discord-player/src/structures/ArrayList.ts b/packages/discord-player/src/structures/ArrayList.ts index ef24d0fa40..14c5cc38ef 100644 --- a/packages/discord-player/src/structures/ArrayList.ts +++ b/packages/discord-player/src/structures/ArrayList.ts @@ -1,8 +1,37 @@ import { randomInt } from 'crypto'; +export interface ArrayListLimitOptions { + name?: string; + maxSize: number; + throwOnFull: boolean; +} + export class ArrayList extends Array { private _prev: T[] = []; + public constructor(private readonly options: ArrayListLimitOptions) { + super(); + + if (options.maxSize < 0) { + throw new Error('Expected "maxItems" to be greater than or equal to 0.'); + } + } + + public push(...items: T[]) { + super.push(...items); + + // Remove the first items if the length is greater than the max items + if (this.options.maxSize > 0 && this.length > this.options.maxSize) { + if (this.options.throwOnFull) { + throw new Error(`Cannot add items to ${this.options.name} as it is full`); + } + + this.splice(0, this.length - this.options.maxSize); + } + + return this.length; + } + public random() { return this[randomInt(this.length)]; } diff --git a/packages/discord-player/src/utils/DependencyReport.ts b/packages/discord-player/src/utils/DependencyReport.ts new file mode 100644 index 0000000000..88e50d40af --- /dev/null +++ b/packages/discord-player/src/utils/DependencyReport.ts @@ -0,0 +1,165 @@ +import { resolve, dirname } from 'node:path'; +import { FFmpeg, FFmpegLib } from '@discord-player/ffmpeg'; + +export interface PackageJSON { + name: string; + version: string; +} + +export type MaybeNull = T | null; + +export interface DependenciesReport { + core: { + 'discord-player': string; + }; + libopus: { + mediaplex: MaybeNull; + '@discordjs/opus': MaybeNull; + '@evan/opus': MaybeNull; + opusscript: MaybeNull; + 'node-opus': MaybeNull; + }; + libsodium: { + 'sodium-native': MaybeNull; + sodium: MaybeNull; + 'libsodium-wrappers': MaybeNull; + tweetnacl: MaybeNull; + 'sodium-javascript': MaybeNull; + }; + ffmpeg: FFmpegReport; +} + +export type FFmpegReport = Record< + FFmpegLib, + MaybeNull<{ + version: string; + hasLibopus: boolean; + }> +>; + +/** + * A utility to generate a report of the dependencies used by the discord-player module. + */ +export const DependencyReport = { + /** + * Finds the package.json file of a package. + * @param dir - The directory to start searching from + * @param packageName - The name of the package to find + * @param depth - The maximum depth to search + * @returns The package.json file, or null if not found + */ + findPackageJSON(dir: string, packageName: string, depth: number): PackageJSON | null { + if (depth === 0) return null; + + const target = resolve(dir, 'package.json'); + + const next = () => DependencyReport.findPackageJSON(resolve(dir, '..'), packageName, depth - 1); + + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const pkgJSON: PackageJSON = require(target); + + if (pkgJSON.name !== packageName) { + return next(); + } + + return pkgJSON; + } catch { + return next(); + } + }, + /** + * Tries to find the version of a dependency. + * @param name - The package to find the version of + * @param maxLookupDepth - The maximum depth to search for the package.json file + * @returns The version of the package, or null if not found + */ + version(name: string, maxLookupDepth = 3): string | null { + try { + if (name === 'discord-player') { + return '[VI]{{inject}}[/VI]'; + } + + const pkg = DependencyReport.findPackageJSON(dirname(require.resolve(name)), name, maxLookupDepth); + return pkg?.version ?? null; + } catch { + return null; + } + }, + /** + * Generates a report of the dependencies used by the discord-player module. + * @returns The report object + */ + generate(): DependenciesReport { + const ffmpegReport = {} as FFmpegReport; + + for (const lib of FFmpeg.sources) { + ffmpegReport[lib.name] = null; + } + + const ffmpeg = FFmpeg.resolveSafe(); + + if (ffmpeg) { + ffmpegReport[ffmpeg.name] = { + hasLibopus: ffmpeg.command.includes('--enable-libopus'), + version: ffmpeg.version + }; + } + + return { + core: { + 'discord-player': DependencyReport.version('discord-player') as string + }, + libopus: { + mediaplex: DependencyReport.version('mediaplex'), + '@discordjs/opus': DependencyReport.version('@discordjs/opus'), + '@evan/opus': DependencyReport.version('@evan/opus'), + opusscript: DependencyReport.version('opusscript'), + 'node-opus': DependencyReport.version('node-opus') + }, + libsodium: { + 'sodium-native': DependencyReport.version('sodium-native'), + sodium: DependencyReport.version('sodium'), + 'libsodium-wrappers': DependencyReport.version('libsodium-wrappers'), + tweetnacl: DependencyReport.version('tweetnacl'), + 'sodium-javascript': DependencyReport.version('sodium-javascript') + }, + ffmpeg: ffmpegReport + }; + }, + /** + * Generates a string representation of the dependencies report. + * @returns The string representation + */ + generateString(): string { + const report = DependencyReport.generate(); + const line = '-'.repeat(50); + + const output: string[] = []; + + output.push('Dependencies Report'); + output.push(line); + + const keys = Object.keys(report) as (keyof DependenciesReport)[]; + + for (const _key of keys) { + const key = _key as keyof DependenciesReport; + + output.push(key); + + const subKeys = Object.keys(report[key]); + + for (const _subKey of subKeys) { + const subKey = _subKey as keyof DependenciesReport[typeof key]; + + output.push(`- ${subKey}: ${report[key][subKey] ?? 'N/A'}`); + } + + output.push(''); + } + + output.push(line); + + return output.join('\n'); + } +}; diff --git a/packages/discord-player/src/voip/connection.ts b/packages/discord-player/src/voip/connection.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/discord-player/src/voip/constants.ts b/packages/discord-player/src/voip/constants.ts deleted file mode 100644 index eae6ee1630..0000000000 --- a/packages/discord-player/src/voip/constants.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const NONCE = Buffer.alloc(24); - -export const OPUS_SAMPLE_RATE = 48000; - -export const OPUS_CHANNELS = 2; - -export const OPUS_FRAME_SIZE = 960; - -export const OPUS_FRAME_DURATION = Math.floor(OPUS_FRAME_SIZE / OPUS_SAMPLE_RATE); - -export const OPUS_SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]); - -export const TIMESTAMP_INC = (OPUS_SAMPLE_RATE / 100) * OPUS_CHANNELS; - -export const MAX_COUNTER_VALUE = 2 ** 32 - 1; - -export const UDP_KEEPALIVE_INTERVAL = 5e3; - -export const MAX_NONCE = 2 ** 32 - 1; - -export const EncryptionMode = { - XSALSA20_POLY1305_LITE: 'xsalsa20_poly1305_lite', - XSALSA20_POLY1305_SUFFIX: 'xsalsa20_poly1305_suffix', - XSALSA20_POLY1305: 'xsalsa20_poly1305' -} as const; - -export type EncryptionMode = (typeof EncryptionMode)[keyof typeof EncryptionMode]; - -export const ENCRYPTION_MODES = new Set(['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'] as const); diff --git a/packages/discord-player/src/voip/libsodium.ts b/packages/discord-player/src/voip/libsodium.ts deleted file mode 100644 index c34401d67b..0000000000 --- a/packages/discord-player/src/voip/libsodium.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { unsafe } from '../common/types'; - -export interface ISodium { - open(buffer: Buffer, nonce: Buffer, key: Uint8Array): Buffer; - close(buffer: Buffer, nonce: Buffer, key: Uint8Array): Buffer; - random(length: number, target?: Buffer): Buffer; -} - -const libs: Record ISodium> = { - 'sodium-native': (lib) => ({ - open(buffer, nonce, key) { - const result = Buffer.allocUnsafe(buffer.length - lib.crypto_secretbox_MACBYTES); - lib.crypto_secretbox_open_easy(result, buffer, nonce, key); - return result; - }, - close(buffer, nonce, key) { - const result = Buffer.allocUnsafe(buffer.length + lib.crypto_secretbox_MACBYTES); - lib.crypto_secretbox_easy(result, buffer, nonce, key); - return result; - }, - random(length, target) { - const buffer = target ?? Buffer.allocUnsafe(length); - lib.randombytes_buf(buffer); - return buffer; - } - }), - sodium: (lib) => ({ - open: lib.api.crypto_secretbox_open_easy, - close: lib.api.crypto_secretbox_easy, - random: (length, target) => { - const buffer = target ?? Buffer.allocUnsafe(length); - lib.api.randombytes_buf(buffer); - return buffer; - } - }), - 'libsodium-wrappers': (lib) => ({ - open: lib.crypto_secretbox_open_easy, - close: lib.crypto_secretbox_easy, - random: lib.randombytes_buf - }), - tweetnacl: (lib) => ({ - open: lib.secretbox.open, - close: lib.secretbox, - random: lib.randombytes_buf - }) -}; - -libs['sodium-javascript'] = libs['sodium-native']; - -const err = () => { - const supported = Object.keys(libs).join(', '); - - throw new Error(`No supported libsodium found. Make sure you have installed one of ${supported} in your project.`); -}; - -const libsodium: ISodium = { - open: err, - close: err, - random: err -}; - -(async () => { - const libsEntries = Object.entries(libs); - - for (const [name, lib] of libsEntries) { - try { - const _sod = await import(name); - const sod = 'default' in _sod ? _sod.default : _sod; - - if (name === 'libsodium-wrappers' && 'ready' in sod) await sod.ready; - - Object.assign(libsodium, lib(sod)); - break; - } catch { - // - } - } -})(); - -export { libsodium }; diff --git a/packages/discord-player/src/voip/networking.ts b/packages/discord-player/src/voip/networking.ts deleted file mode 100644 index e7841e24f2..0000000000 --- a/packages/discord-player/src/voip/networking.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { EventEmitter } from '../common/EventEmitter'; -import { EncryptionMode, MAX_NONCE, NONCE } from './constants'; -import { libsodium } from './libsodium'; - -export const VoipEvents = {} as const; - -export type VoipEvents = (typeof VoipEvents)[keyof typeof VoipEvents]; - -export interface VoipEventsMap {} - -export interface NetworkingOptions {} - -export interface ConnectionData { - encryptionMode: EncryptionMode; - nonce: number; - nonceBuffer: Buffer; - packetsPlayed: number; - secretKey: Uint8Array; - sequence: number; - speaking: boolean; - ssrc: number; - timestamp: number; -} - -export class Networking extends EventEmitter { - public constructor(public readonly options: NetworkingOptions) { - super(); - } - - #createAudioPacket(opus: Buffer, data: ConnectionData) { - const packetBuffer = Buffer.alloc(12); - packetBuffer[0] = 0x80; - packetBuffer[1] = 0x78; - - const { sequence, timestamp, ssrc } = data; - - packetBuffer.writeUIntBE(sequence, 2, 2); - packetBuffer.writeUIntBE(timestamp, 4, 4); - packetBuffer.writeUIntBE(ssrc, 8, 4); - - packetBuffer.copy(NONCE, 0, 0, 12); - - return Buffer.concat([packetBuffer, ...this.#encrypt(opus, data)]); - } - - #encrypt(opus: Buffer, data: ConnectionData) { - switch (data.encryptionMode) { - case EncryptionMode.XSALSA20_POLY1305_LITE: { - data.nonce++; - if (data.nonce > MAX_NONCE) data.nonce = 0; - data.nonceBuffer.writeUInt32BE(data.nonce, 0); - - return [libsodium.close(opus, data.nonceBuffer, data.secretKey)]; - } - case EncryptionMode.XSALSA20_POLY1305_SUFFIX: { - const random = libsodium.random(24, data.nonceBuffer); - - return [libsodium.close(opus, random, data.secretKey), random]; - } - default: { - return [libsodium.close(opus, data.nonceBuffer, data.secretKey)]; - } - } - } -} diff --git a/packages/discord-player/src/voip/udp.ts b/packages/discord-player/src/voip/udp.ts deleted file mode 100644 index eb0e38560c..0000000000 --- a/packages/discord-player/src/voip/udp.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { isIPv4 } from 'node:net'; -import { createSocket, type Socket } from 'node:dgram'; -import { EventEmitter } from '../common/EventEmitter'; -import { MAX_COUNTER_VALUE, UDP_KEEPALIVE_INTERVAL } from './constants'; - -export interface RemoteAddress { - address: string; - port: number; -} - -export interface VoipUdpOptions { - address: string; -} - -export const VoipUdpEvents = { - Error: 'error', - Close: 'close', - Message: 'message' -} as const; - -export type VoipUdpEvents = (typeof VoipUdpEvents)[keyof typeof VoipUdpEvents]; - -export interface VoipUdpEventsMap { - [VoipUdpEvents.Error]: (error: Error) => void; - [VoipUdpEvents.Close]: () => void; - [VoipUdpEvents.Message]: (data: Buffer) => void; -} - -export class VoipUdpSocket extends EventEmitter { - private readonly socket: Socket; - private keepAliveBuffer: Buffer; - private keepAliveInterval: NodeJS.Timeout; - private keepAliveCounter = 0; - - public constructor(private readonly remoteAddress: RemoteAddress) { - super(); - - this.socket = createSocket('udp4'); - this.socket.on('error', this.#onError.bind(this)); - this.socket.on('message', this.#onMessage.bind(this)); - this.socket.on('close', this.#onClose.bind(this)); - - this.keepAliveBuffer = Buffer.alloc(0); - this.keepAliveInterval = setInterval(this.#keepAlive.bind(this), UDP_KEEPALIVE_INTERVAL); - - setImmediate(() => this.#keepAlive()); - } - - public destroy() { - try { - this.debug?.('Destroying the UDP socket'); - this.socket.close(); - } catch { - // - } - - clearInterval(this.keepAliveInterval); - } - - public send(buffer: Buffer) { - const { address, port } = this.remoteAddress; - - this.socket.send(buffer, port, address); - } - - public performIPDiscovery(ssrc: number): Promise { - return new Promise((resolve, reject) => { - const listener = (message: Buffer) => { - try { - if (message.readUInt16BE(0) !== 2) return; - - const data = this.#parseLocalPacket(message); - this.socket.off('message', listener); - this.socket.off('close', rejectListener); - resolve(data); - } catch { - // - } - }; - - const rejectListener = () => reject(new Error('Socket closed before performing IP discovery')); - - this.socket.on('message', listener); - this.socket.once('close', rejectListener); - - const buffer = Buffer.alloc(74); - - buffer.writeUInt16BE(1, 0); - buffer.writeUInt16BE(70, 2); - buffer.writeUInt32BE(ssrc, 4); - - this.send(buffer); - }); - } - - #parseLocalPacket(buffer: Buffer): RemoteAddress { - const msg = Buffer.from(buffer); - - const address = msg.subarray(8, msg.indexOf(0, 8)).toString('utf-8'); - - if (!isIPv4(address)) { - throw new Error('Invalid IP address received'); - } - - const port = msg.readUInt16BE(msg.length - 2); - - return { - address, - port - } satisfies RemoteAddress; - } - - #onError(error: Error) { - this.emit(VoipUdpEvents.Error, error); - } - - #onClose() { - this.emit(VoipUdpEvents.Close); - } - - #onMessage(data: Buffer) { - this.emit(VoipUdpEvents.Message, data); - } - - #keepAlive() { - this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0); - this.send(this.keepAliveBuffer); - this.keepAliveCounter++; - - if (this.keepAliveCounter > MAX_COUNTER_VALUE) { - this.keepAliveCounter = 0; - } - } -} diff --git a/packages/discord-player/src/voip/websocket.ts b/packages/discord-player/src/voip/websocket.ts deleted file mode 100644 index b1ae283a83..0000000000 --- a/packages/discord-player/src/voip/websocket.ts +++ /dev/null @@ -1,117 +0,0 @@ -import WebSocket from 'ws'; -import { EventEmitter } from '../common/EventEmitter'; -import type { unsafe } from '../common/types'; -import { VoiceOpcodes } from 'discord-api-types/voice/v4'; - -export interface VoipWebSocketOptions { - address: string; -} - -export const VoipWebSocketEvents = { - Error: 'error', - Open: 'open', - Close: 'close', - Packet: 'packet' -} as const; - -export type VoipWebSocketEvents = (typeof VoipWebSocketEvents)[keyof typeof VoipWebSocketEvents]; - -export interface VoipWebSocketEventsMap { - [VoipWebSocketEvents.Error]: (error: Error) => void; - [VoipWebSocketEvents.Open]: (event: WebSocket.Event) => void; - [VoipWebSocketEvents.Close]: (event: WebSocket.CloseEvent) => void; - [VoipWebSocketEvents.Packet]: (data: unsafe) => void; -} - -export class VoipWebSocket extends EventEmitter { - private readonly ws: WebSocket; - private lastHeartbeatSent = 0; - private missedHeartbeats = 0; - private heartbeatInterval: NodeJS.Timeout | undefined; - - public constructor(private readonly options: VoipWebSocketOptions) { - super(); - - this.ws = new WebSocket(options.address); - this.ws.onmessage = this.#onMessage.bind(this); - this.ws.onerror = this.#onError.bind(this); - this.ws.onopen = this.#onOpen.bind(this); - this.ws.onclose = this.#onClose.bind(this); - } - - public destroy() { - try { - this.debug?.('Destroying the WebSocket'); - this.setHeartbeat(0); - this.ws.close(1000); - } catch (error) { - this.emit(VoipWebSocketEvents.Error, error as Error); - } - } - - public send(data: unsafe) { - try { - const payload = JSON.stringify(data); - this.debug?.('Sending payload ${payload}'); - this.ws.send(payload); - } catch (error) { - this.emit(VoipWebSocketEvents.Error, error as Error); - } - } - - public setHeartbeat(interval: number) { - if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); - if (interval <= 0) return; - - this.heartbeatInterval = setInterval(() => { - if (this.lastHeartbeatSent !== 0 && this.missedHeartbeats >= 3) { - this.debug?.('Missed too many heartbeats'); - this.ws.close(); - this.setHeartbeat(0); - return; - } - - this.#heartbeat(); - }, interval); - } - - #heartbeat() { - this.lastHeartbeatSent = Date.now(); - this.missedHeartbeats++; - - this.send({ - op: VoiceOpcodes.Heartbeat, - d: this.lastHeartbeatSent - }); - } - - #onMessage(event: WebSocket.MessageEvent) { - if (typeof event.data !== 'string') return; - - try { - const data = JSON.parse(event.data); - - this.debug?.('Received payload ${event.data}'); - - if (data.op === VoiceOpcodes.HeartbeatAck) { - this.missedHeartbeats = 0; - } - - this.emit(VoipWebSocketEvents.Packet, data); - } catch (error) { - this.emit(VoipWebSocketEvents.Error, error as Error); - } - } - - #onError(event: WebSocket.ErrorEvent | Error) { - this.emit(VoipWebSocketEvents.Error, event instanceof Error ? event : event.error); - } - - #onOpen(event: WebSocket.Event) { - this.emit(VoipWebSocketEvents.Open, event); - } - - #onClose(event: WebSocket.CloseEvent) { - this.emit(VoipWebSocketEvents.Close, event); - } -} diff --git a/packages/discord-player/tsconfig.json b/packages/discord-player/tsconfig.json index ff19098dc3..f32db5c361 100644 --- a/packages/discord-player/tsconfig.json +++ b/packages/discord-player/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@discord-player/tsconfig/base.json", - "include": ["src/**/*"] -} \ No newline at end of file + "include": ["src/**/*", "../discord-voip"] +} diff --git a/packages/discord-voip/README.md b/packages/discord-voip/README.md new file mode 100644 index 0000000000..c016aa8526 --- /dev/null +++ b/packages/discord-voip/README.md @@ -0,0 +1,6 @@ +# Discord VoIP + +This directory contains the VoIP implementation for Discord Player. It is based on [@discordjs/voice](https://npm.im/@discordjs/voice) library with some modifications to make it work with Discord Player. Some of the notable modifications include: + +- Removal of the `VoiceReceiver` and its components. +- Modified `SecretBox` diff --git a/packages/discord-voip/package.json b/packages/discord-voip/package.json new file mode 100644 index 0000000000..f2540419b9 --- /dev/null +++ b/packages/discord-voip/package.json @@ -0,0 +1,41 @@ +{ + "name": "discord-voip", + "version": "2.0.0", + "description": "Discord VoIP library used by discord-player", + "keywords": [ + "discord-player", + "music", + "bot", + "discord.js", + "javascript", + "voip", + "lavalink", + "lavaplayer" + ], + "author": "Androz2091 ", + "homepage": "https://discord-player.js.org", + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Androz2091/discord-player.git" + }, + "scripts": { + "build": "tsup && node ./scripts/esm-shim.cjs", + "build:check": "tsc --noEmit", + "lint": "eslint src --ext .ts --fix" + }, + "bugs": { + "url": "https://github.com/Androz2091/discord-player/issues" + }, + "devDependencies": { + "@discord-player/tsconfig": "workspace:^", + "tsup": "^7.2.0", + "typescript": "^5.2.2" + } +} diff --git a/packages/discord-voip/src/DataStore.ts b/packages/discord-voip/src/DataStore.ts new file mode 100644 index 0000000000..00a38eb09b --- /dev/null +++ b/packages/discord-voip/src/DataStore.ts @@ -0,0 +1,192 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import { GatewayOpcodes } from 'discord-api-types/v10'; +import type { VoiceConnection } from './VoiceConnection'; +import type { AudioPlayer } from './audio/index'; + +export interface JoinConfig { + channelId: string | null; + group: string; + guildId: string; + selfDeaf: boolean; + selfMute: boolean; +} + +/** + * Sends a voice state update to the main websocket shard of a guild, to indicate joining/leaving/moving across + * voice channels. + * + * @param config - The configuration to use when joining the voice channel + */ +export function createJoinVoiceChannelPayload(config: JoinConfig) { + return { + op: GatewayOpcodes.VoiceStateUpdate, + // eslint-disable-next-line id-length + d: { + guild_id: config.guildId, + channel_id: config.channelId, + self_deaf: config.selfDeaf, + self_mute: config.selfMute + } + }; +} + +// Voice Connections +const groups = new Map>(); +groups.set('default', new Map()); + +function getOrCreateGroup(group: string) { + const existing = groups.get(group); + if (existing) return existing; + const map = new Map(); + groups.set(group, map); + return map; +} + +/** + * Retrieves the map of group names to maps of voice connections. By default, all voice connections + * are created under the 'default' group. + * + * @returns The group map + */ +export function getGroups() { + return groups; +} + +/** + * Retrieves all the voice connections under the 'default' group. + * + * @param group - The group to look up + * @returns The map of voice connections + */ +export function getVoiceConnections(group?: 'default'): Map; + +/** + * Retrieves all the voice connections under the given group name. + * + * @param group - The group to look up + * @returns The map of voice connections + */ +export function getVoiceConnections(group: string): Map | undefined; + +/** + * Retrieves all the voice connections under the given group name. Defaults to the 'default' group. + * + * @param group - The group to look up + * @returns The map of voice connections + */ +export function getVoiceConnections(group = 'default') { + return groups.get(group); +} + +/** + * Finds a voice connection with the given guild id and group. Defaults to the 'default' group. + * + * @param guildId - The guild id of the voice connection + * @param group - the group that the voice connection was registered with + * @returns The voice connection, if it exists + */ +export function getVoiceConnection(guildId: string, group = 'default') { + return getVoiceConnections(group)?.get(guildId); +} + +export function untrackVoiceConnection(voiceConnection: VoiceConnection) { + return getVoiceConnections(voiceConnection.joinConfig.group)?.delete(voiceConnection.joinConfig.guildId); +} + +export function trackVoiceConnection(voiceConnection: VoiceConnection) { + return getOrCreateGroup(voiceConnection.joinConfig.group).set(voiceConnection.joinConfig.guildId, voiceConnection); +} + +// Audio Players + +// Each audio packet is 20ms long +const FRAME_LENGTH = 20; + +let audioCycleInterval: NodeJS.Timeout | undefined; +let nextTime = -1; + +/** + * A list of created audio players that are still active and haven't been destroyed. + */ +const audioPlayers: AudioPlayer[] = []; + +/** + * Called roughly every 20 milliseconds. Dispatches audio from all players, and then gets the players to prepare + * the next audio frame. + */ +function audioCycleStep() { + if (nextTime === -1) return; + + nextTime += FRAME_LENGTH; + const available = audioPlayers.filter((player) => player.checkPlayable()); + + for (const player of available) { + // eslint-disable-next-line @typescript-eslint/dot-notation + player['_stepDispatch'](); + } + + prepareNextAudioFrame(available); +} + +/** + * Recursively gets the players that have been passed as parameters to prepare audio frames that can be played + * at the start of the next cycle. + */ +function prepareNextAudioFrame(players: AudioPlayer[]) { + const nextPlayer = players.shift(); + + if (!nextPlayer) { + if (nextTime !== -1) { + audioCycleInterval = setTimeout(() => audioCycleStep(), nextTime - Date.now()); + } + + return; + } + + // eslint-disable-next-line @typescript-eslint/dot-notation + nextPlayer['_stepPrepare'](); + + // setImmediate to avoid long audio player chains blocking other scheduled tasks + setImmediate(() => prepareNextAudioFrame(players)); +} + +/** + * Checks whether or not the given audio player is being driven by the data store clock. + * + * @param target - The target to test for + * @returns `true` if it is being tracked, `false` otherwise + */ +export function hasAudioPlayer(target: AudioPlayer) { + return audioPlayers.includes(target); +} + +/** + * Adds an audio player to the data store tracking list, if it isn't already there. + * + * @param player - The player to track + */ +export function addAudioPlayer(player: AudioPlayer) { + if (hasAudioPlayer(player)) return player; + audioPlayers.push(player); + if (audioPlayers.length === 1) { + nextTime = Date.now(); + setImmediate(() => audioCycleStep()); + } + + return player; +} + +/** + * Removes an audio player from the data store tracking list, if it is present there. + */ +export function deleteAudioPlayer(player: AudioPlayer) { + const index = audioPlayers.indexOf(player); + if (index === -1) return; + audioPlayers.splice(index, 1); + if (audioPlayers.length === 0) { + nextTime = -1; + if (audioCycleInterval !== undefined) clearTimeout(audioCycleInterval); + } +} diff --git a/packages/discord-voip/src/VoiceConnection.ts b/packages/discord-voip/src/VoiceConnection.ts new file mode 100644 index 0000000000..ca270d2289 --- /dev/null +++ b/packages/discord-voip/src/VoiceConnection.ts @@ -0,0 +1,689 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +/* eslint-disable @typescript-eslint/unbound-method */ +import type { Buffer } from 'node:buffer'; +import { EventEmitter } from 'node:events'; +import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10'; +import type { JoinConfig } from './DataStore'; +import { getVoiceConnection, createJoinVoiceChannelPayload, trackVoiceConnection, untrackVoiceConnection } from './DataStore'; +import type { AudioPlayer } from './audio/AudioPlayer'; +import type { PlayerSubscription } from './audio/PlayerSubscription'; +import { Networking, NetworkingStatusCode, type NetworkingState } from './networking/Networking'; +import type { DiscordGatewayAdapterImplementerMethods } from './util/adapter'; +import { noop } from './util/util'; +import type { CreateVoiceConnectionOptions } from './index'; +import { unsafe } from './common/types'; + +/** + * The various status codes a voice connection can hold at any one time. + */ +export enum VoiceConnectionStatus { + /** + * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection. + */ + Connecting = 'connecting', + + /** + * The voice connection has been destroyed and untracked, it cannot be reused. + */ + Destroyed = 'destroyed', + + /** + * The voice connection has either been severed or not established. + */ + Disconnected = 'disconnected', + + /** + * A voice connection has been established, and is ready to be used. + */ + Ready = 'ready', + + /** + * Sending a packet to the main Discord gateway to indicate we want to change our voice state. + */ + Signalling = 'signalling' +} + +/** + * The state that a VoiceConnection will be in when it is waiting to receive a VOICE_SERVER_UPDATE and + * VOICE_STATE_UPDATE packet from Discord, provided by the adapter. + */ +export interface VoiceConnectionSignallingState { + adapter: DiscordGatewayAdapterImplementerMethods; + status: VoiceConnectionStatus.Signalling; + subscription?: PlayerSubscription | undefined; +} + +/** + * The reasons a voice connection can be in the disconnected state. + */ +export enum VoiceConnectionDisconnectReason { + /** + * When the WebSocket connection has been closed. + */ + WebSocketClose, + + /** + * When the adapter was unable to send a message requested by the VoiceConnection. + */ + AdapterUnavailable, + + /** + * When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed. + */ + EndpointRemoved, + + /** + * When a manual disconnect was requested. + */ + Manual +} + +/** + * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is + * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. + */ +export interface VoiceConnectionDisconnectedBaseState { + adapter: DiscordGatewayAdapterImplementerMethods; + status: VoiceConnectionStatus.Disconnected; + subscription?: PlayerSubscription | undefined; +} + +/** + * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is + * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect. + */ +export interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDisconnectedBaseState { + reason: Exclude; +} + +/** + * The state that a VoiceConnection will be in when its WebSocket connection was closed. + * You can manually attempt to reconnect using VoiceConnection#reconnect. + */ +export interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState { + /** + * The close code of the WebSocket connection to the Discord voice server. + */ + closeCode: number; + + reason: VoiceConnectionDisconnectReason.WebSocketClose; +} + +/** + * The states that a VoiceConnection can be in when it is not connected to a Discord voice server nor is + * it attempting to connect. You can manually attempt to connect using VoiceConnection#reconnect. + */ +export type VoiceConnectionDisconnectedState = VoiceConnectionDisconnectedOtherState | VoiceConnectionDisconnectedWebSocketState; + +/** + * The state that a VoiceConnection will be in when it is establishing a connection to a Discord + * voice server. + */ +export interface VoiceConnectionConnectingState { + adapter: DiscordGatewayAdapterImplementerMethods; + networking: Networking; + status: VoiceConnectionStatus.Connecting; + subscription?: PlayerSubscription | undefined; +} + +/** + * The state that a VoiceConnection will be in when it has an active connection to a Discord + * voice server. + */ +export interface VoiceConnectionReadyState { + adapter: DiscordGatewayAdapterImplementerMethods; + networking: Networking; + status: VoiceConnectionStatus.Ready; + subscription?: PlayerSubscription | undefined; +} + +/** + * The state that a VoiceConnection will be in when it has been permanently been destroyed by the + * user and untracked by the library. It cannot be reconnected, instead, a new VoiceConnection + * needs to be established. + */ +export interface VoiceConnectionDestroyedState { + status: VoiceConnectionStatus.Destroyed; +} + +/** + * The various states that a voice connection can be in. + */ +export type VoiceConnectionState = VoiceConnectionConnectingState | VoiceConnectionDestroyedState | VoiceConnectionDisconnectedState | VoiceConnectionReadyState | VoiceConnectionSignallingState; + +export interface VoiceConnection extends EventEmitter { + /** + * Emitted when there is an error emitted from the voice connection + * + * @eventProperty + */ + on(event: 'error', listener: (error: Error) => void): this; + /** + * Emitted debugging information about the voice connection + * + * @eventProperty + */ + on(event: 'debug', listener: (message: string) => void): this; + /** + * Emitted when the state of the voice connection changes + * + * @eventProperty + */ + on(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this; + /** + * Emitted when the state of the voice connection changes to a specific status + * + * @eventProperty + */ + on(event: Event, listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState & { status: Event }) => void): this; +} + +/** + * A connection to the voice server of a Guild, can be used to play audio in voice channels. + */ +export class VoiceConnection extends EventEmitter { + /** + * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin. + * When a connection is successfully established, it resets to 0. + */ + public rejoinAttempts: number; + + /** + * The state of the voice connection. + */ + private _state: VoiceConnectionState; + + /** + * A configuration storing all the data needed to reconnect to a Guild's voice server. + * + * @internal + */ + public readonly joinConfig: JoinConfig; + + /** + * The two packets needed to successfully establish a voice connection. They are received + * from the main Discord gateway after signalling to change the voice state. + */ + private readonly packets: { + server: GatewayVoiceServerUpdateDispatchData | undefined; + state: GatewayVoiceStateUpdateDispatchData | undefined; + }; + + /** + * The debug logger function, if debugging is enabled. + */ + private readonly debug: ((message: string) => void) | null; + + /** + * Creates a new voice connection. + * + * @param joinConfig - The data required to establish the voice connection + * @param options - The options used to create this voice connection + */ + public constructor(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions) { + super(); + + this.debug = options.debug ? (message: string) => this.emit('debug', message) : null; + this.rejoinAttempts = 0; + + this.onNetworkingClose = this.onNetworkingClose.bind(this); + this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this); + this.onNetworkingError = this.onNetworkingError.bind(this); + this.onNetworkingDebug = this.onNetworkingDebug.bind(this); + + const adapter = options.adapterCreator({ + onVoiceServerUpdate: (data) => this.addServerPacket(data), + onVoiceStateUpdate: (data) => this.addStatePacket(data), + destroy: () => this.destroy(false) + }); + + this._state = { status: VoiceConnectionStatus.Signalling, adapter }; + + this.packets = { + server: undefined, + state: undefined + }; + + this.joinConfig = joinConfig; + } + + /** + * The current state of the voice connection. + */ + public get state() { + return this._state; + } + + /** + * Updates the state of the voice connection, performing clean-up operations where necessary. + */ + public set state(newState: VoiceConnectionState) { + const oldState = this._state; + const oldNetworking = Reflect.get(oldState, 'networking') as Networking | undefined; + const newNetworking = Reflect.get(newState, 'networking') as Networking | undefined; + + const oldSubscription = Reflect.get(oldState, 'subscription') as PlayerSubscription | undefined; + const newSubscription = Reflect.get(newState, 'subscription') as PlayerSubscription | undefined; + + if (oldNetworking !== newNetworking) { + if (oldNetworking) { + oldNetworking.on('error', noop); + oldNetworking.off('debug', this.onNetworkingDebug); + oldNetworking.off('error', this.onNetworkingError); + oldNetworking.off('close', this.onNetworkingClose); + oldNetworking.off('stateChange', this.onNetworkingStateChange); + oldNetworking.destroy(); + } + } + + if (newState.status === VoiceConnectionStatus.Ready) { + this.rejoinAttempts = 0; + } + + // If destroyed, the adapter can also be destroyed so it can be cleaned up by the user + if (oldState.status !== VoiceConnectionStatus.Destroyed && newState.status === VoiceConnectionStatus.Destroyed) { + oldState.adapter.destroy(); + } + + this._state = newState; + + if (oldSubscription && oldSubscription !== newSubscription) { + oldSubscription.unsubscribe(); + } + + this.emit('stateChange', oldState, newState); + if (oldState.status !== newState.status) { + this.emit(newState.status, oldState, newState as unsafe); + } + } + + /** + * Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the + * new data provided in the packet. + * + * @param packet - The received `VOICE_SERVER_UPDATE` packet + */ + private addServerPacket(packet: GatewayVoiceServerUpdateDispatchData) { + this.packets.server = packet; + if (packet.endpoint) { + this.configureNetworking(); + } else if (this.state.status !== VoiceConnectionStatus.Destroyed) { + this.state = { + ...this.state, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.EndpointRemoved + }; + } + } + + /** + * Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the + * channel that the client is connected to. + * + * @param packet - The received `VOICE_STATE_UPDATE` packet + */ + private addStatePacket(packet: GatewayVoiceStateUpdateDispatchData) { + this.packets.state = packet; + + if (packet.self_deaf !== undefined) this.joinConfig.selfDeaf = packet.self_deaf; + if (packet.self_mute !== undefined) this.joinConfig.selfMute = packet.self_mute; + if (packet.channel_id) this.joinConfig.channelId = packet.channel_id; + /* + the channel_id being null doesn't necessarily mean it was intended for the client to leave the voice channel + as it may have disconnected due to network failure. This will be gracefully handled once the voice websocket + dies, and then it is up to the user to decide how they wish to handle this. + */ + } + + /** + * Attempts to configure a networking instance for this voice connection using the received packets. + * Both packets are required, and any existing networking instance will be destroyed. + * + * @remarks + * This is called when the voice server of the connection changes, e.g. if the bot is moved into a + * different channel in the same guild but has a different voice server. In this instance, the connection + * needs to be re-established to the new voice server. + * + * The connection will transition to the Connecting state when this is called. + */ + public configureNetworking() { + const { server, state } = this.packets; + if (!server || !state || this.state.status === VoiceConnectionStatus.Destroyed || !server.endpoint) return; + + const networking = new Networking( + { + endpoint: server.endpoint, + serverId: server.guild_id, + token: server.token, + sessionId: state.session_id, + userId: state.user_id + }, + Boolean(this.debug) + ); + + networking.once('close', this.onNetworkingClose); + networking.on('stateChange', this.onNetworkingStateChange); + networking.on('error', this.onNetworkingError); + networking.on('debug', this.onNetworkingDebug); + + this.state = { + ...this.state, + status: VoiceConnectionStatus.Connecting, + networking + }; + } + + /** + * Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect), + * the voice connection will transition to the Disconnected state which will store the close code. You can + * decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect(). + * + * @remarks + * If the close code was anything other than 4014, it is likely that the closing was not intended, and so the + * VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts + * to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state. + * @param code - The close code + */ + private onNetworkingClose(code: number) { + if (this.state.status === VoiceConnectionStatus.Destroyed) return; + // If networking closes, try to connect to the voice channel again. + if (code === 4_014) { + // Disconnected - networking is already destroyed here + this.state = { + ...this.state, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.WebSocketClose, + closeCode: code + }; + } else { + this.state = { + ...this.state, + status: VoiceConnectionStatus.Signalling + }; + this.rejoinAttempts++; + if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { + this.state = { + ...this.state, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.AdapterUnavailable + }; + } + } + } + + /** + * Called when the state of the networking instance changes. This is used to derive the state of the voice connection. + * + * @param oldState - The previous state + * @param newState - The new state + */ + private onNetworkingStateChange(oldState: NetworkingState, newState: NetworkingState) { + if (oldState.code === newState.code) return; + if (this.state.status !== VoiceConnectionStatus.Connecting && this.state.status !== VoiceConnectionStatus.Ready) return; + + if (newState.code === NetworkingStatusCode.Ready) { + this.state = { + ...this.state, + status: VoiceConnectionStatus.Ready + }; + } else if (newState.code !== NetworkingStatusCode.Closed) { + this.state = { + ...this.state, + status: VoiceConnectionStatus.Connecting + }; + } + } + + /** + * Propagates errors from the underlying network instance. + * + * @param error - The error to propagate + */ + private onNetworkingError(error: Error) { + this.emit('error', error); + } + + /** + * Propagates debug messages from the underlying network instance. + * + * @param message - The debug message to propagate + */ + private onNetworkingDebug(message: string) { + this.debug?.(`[NW] ${message}`); + } + + /** + * Prepares an audio packet for dispatch. + * + * @param buffer - The Opus packet to prepare + */ + public prepareAudioPacket(buffer: Buffer) { + const state = this.state; + if (state.status !== VoiceConnectionStatus.Ready) return; + return state.networking.prepareAudioPacket(buffer); + } + + /** + * Dispatches the previously prepared audio packet (if any) + */ + public dispatchAudio() { + const state = this.state; + if (state.status !== VoiceConnectionStatus.Ready) return; + return state.networking.dispatchAudio(); + } + + /** + * Prepares an audio packet and dispatches it immediately. + * + * @param buffer - The Opus packet to play + */ + public playOpusPacket(buffer: Buffer) { + const state = this.state; + if (state.status !== VoiceConnectionStatus.Ready) return; + state.networking.prepareAudioPacket(buffer); + return state.networking.dispatchAudio(); + } + + /** + * Destroys the VoiceConnection, preventing it from connecting to voice again. + * This method should be called when you no longer require the VoiceConnection to + * prevent memory leaks. + * + * @param adapterAvailable - Whether the adapter can be used + */ + public destroy(adapterAvailable = true) { + if (this.state.status === VoiceConnectionStatus.Destroyed) { + throw new Error('Cannot destroy VoiceConnection - it has already been destroyed'); + } + + if (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) { + untrackVoiceConnection(this); + } + + if (adapterAvailable) { + this.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null })); + } + + this.state = { + status: VoiceConnectionStatus.Destroyed + }; + } + + /** + * Disconnects the VoiceConnection, allowing the possibility of rejoining later on. + * + * @returns `true` if the connection was successfully disconnected + */ + public disconnect() { + if (this.state.status === VoiceConnectionStatus.Destroyed || this.state.status === VoiceConnectionStatus.Signalling) { + return false; + } + + this.joinConfig.channelId = null; + if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { + this.state = { + adapter: this.state.adapter, + subscription: this.state.subscription, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.AdapterUnavailable + }; + return false; + } + + this.state = { + adapter: this.state.adapter, + reason: VoiceConnectionDisconnectReason.Manual, + status: VoiceConnectionStatus.Disconnected + }; + return true; + } + + /** + * Attempts to rejoin (better explanation soon:tm:) + * + * @remarks + * Calling this method successfully will automatically increment the `rejoinAttempts` counter, + * which you can use to inform whether or not you'd like to keep attempting to reconnect your + * voice connection. + * + * A state transition from Disconnected to Signalling will be observed when this is called. + */ + public rejoin(joinConfig?: Omit) { + if (this.state.status === VoiceConnectionStatus.Destroyed) { + return false; + } + + const notReady = this.state.status !== VoiceConnectionStatus.Ready; + + if (notReady) this.rejoinAttempts++; + Object.assign(this.joinConfig, joinConfig); + if (this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) { + if (notReady) { + this.state = { + ...this.state, + status: VoiceConnectionStatus.Signalling + }; + } + + return true; + } + + this.state = { + adapter: this.state.adapter, + subscription: this.state.subscription, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.AdapterUnavailable + }; + return false; + } + + /** + * Updates the speaking status of the voice connection. This is used when audio players are done playing audio, + * and need to signal that the connection is no longer playing audio. + * + * @param enabled - Whether or not to show as speaking + */ + public setSpeaking(enabled: boolean) { + if (this.state.status !== VoiceConnectionStatus.Ready) return false; + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + return this.state.networking.setSpeaking(enabled); + } + + /** + * Subscribes to an audio player, allowing the player to play audio on this voice connection. + * + * @param player - The audio player to subscribe to + * @returns The created subscription + */ + public subscribe(player: AudioPlayer) { + if (this.state.status === VoiceConnectionStatus.Destroyed) return; + + // eslint-disable-next-line @typescript-eslint/dot-notation + const subscription = player['subscribe'](this); + + this.state = { + ...this.state, + subscription + }; + + return subscription; + } + + /** + * The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice + * connection, if this data is available. + * + * @remarks + * For this data to be available, the VoiceConnection must be in the Ready state, and its underlying + * WebSocket connection and UDP socket must have had at least one ping-pong exchange. + */ + public get ping() { + if (this.state.status === VoiceConnectionStatus.Ready && this.state.networking.state.code === NetworkingStatusCode.Ready) { + return { + ws: this.state.networking.state.ws.ping, + udp: this.state.networking.state.udp.ping + }; + } + + return { + ws: undefined, + udp: undefined + }; + } + + /** + * Called when a subscription of this voice connection to an audio player is removed. + * + * @param subscription - The removed subscription + */ + protected onSubscriptionRemoved(subscription: PlayerSubscription) { + if (this.state.status !== VoiceConnectionStatus.Destroyed && this.state.subscription === subscription) { + this.state = { + ...this.state, + subscription: undefined + }; + } + } +} + +/** + * Creates a new voice connection. + * + * @param joinConfig - The data required to establish the voice connection + * @param options - The options to use when joining the voice channel + */ +export function createVoiceConnection(joinConfig: JoinConfig, options: CreateVoiceConnectionOptions) { + const payload = createJoinVoiceChannelPayload(joinConfig); + const existing = getVoiceConnection(joinConfig.guildId, joinConfig.group); + if (existing && existing.state.status !== VoiceConnectionStatus.Destroyed) { + if (existing.state.status === VoiceConnectionStatus.Disconnected) { + existing.rejoin({ + channelId: joinConfig.channelId, + selfDeaf: joinConfig.selfDeaf, + selfMute: joinConfig.selfMute + }); + } else if (!existing.state.adapter.sendPayload(payload)) { + existing.state = { + ...existing.state, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.AdapterUnavailable + }; + } + + return existing; + } + + const voiceConnection = new VoiceConnection(joinConfig, options); + trackVoiceConnection(voiceConnection); + if (voiceConnection.state.status !== VoiceConnectionStatus.Destroyed && !voiceConnection.state.adapter.sendPayload(payload)) { + voiceConnection.state = { + ...voiceConnection.state, + status: VoiceConnectionStatus.Disconnected, + reason: VoiceConnectionDisconnectReason.AdapterUnavailable + }; + } + + return voiceConnection; +} diff --git a/packages/discord-voip/src/audio/AudioPlayer.ts b/packages/discord-voip/src/audio/AudioPlayer.ts new file mode 100644 index 0000000000..61e8d8e5f8 --- /dev/null +++ b/packages/discord-voip/src/audio/AudioPlayer.ts @@ -0,0 +1,637 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +/* eslint-disable @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/method-signature-style */ +import { Buffer } from 'node:buffer'; +import { EventEmitter } from 'node:events'; +import { addAudioPlayer, deleteAudioPlayer } from '../DataStore'; +import { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection'; +import { noop } from '../util/util'; +import { AudioPlayerError } from './AudioPlayerError'; +import type { AudioResource } from './AudioResource'; +import { PlayerSubscription } from './PlayerSubscription'; +import { unsafe } from '../common/types'; + +// The Opus "silent" frame +export const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]); + +/** + * Describes the behavior of the player when an audio packet is played but there are no available + * voice connections to play to. + */ +export enum NoSubscriberBehavior { + /** + * Pauses playing the stream until a voice connection becomes available. + */ + Pause = 'pause', + + /** + * Continues to play through the resource regardless. + */ + Play = 'play', + + /** + * The player stops and enters the Idle state. + */ + Stop = 'stop' +} + +export enum AudioPlayerStatus { + /** + * When the player has paused itself. Only possible with the "pause" no subscriber behavior. + */ + AutoPaused = 'autopaused', + + /** + * When the player is waiting for an audio resource to become readable before transitioning to Playing. + */ + Buffering = 'buffering', + + /** + * When there is currently no resource for the player to be playing. + */ + Idle = 'idle', + + /** + * When the player has been manually paused. + */ + Paused = 'paused', + + /** + * When the player is actively playing an audio resource. + */ + Playing = 'playing' +} + +/** + * Options that can be passed when creating an audio player, used to specify its behavior. + */ +export interface CreateAudioPlayerOptions { + behaviors?: { + maxMissedFrames?: number; + noSubscriber?: NoSubscriberBehavior; + }; + debug?: boolean; +} + +/** + * The state that an AudioPlayer is in when it has no resource to play. This is the starting state. + */ +export interface AudioPlayerIdleState { + status: AudioPlayerStatus.Idle; +} + +/** + * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this + * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then + * it will re-enter the Idle state. + */ +export interface AudioPlayerBufferingState { + onFailureCallback: () => void; + onReadableCallback: () => void; + onStreamError: (error: Error) => void; + /** + * The resource that the AudioPlayer is waiting for + */ + resource: AudioResource; + status: AudioPlayerStatus.Buffering; +} + +/** + * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends, + * it will enter the Idle state. + */ +export interface AudioPlayerPlayingState { + /** + * The number of consecutive times that the audio resource has been unable to provide an Opus frame. + */ + missedFrames: number; + onStreamError: (error: Error) => void; + + /** + * The playback duration in milliseconds of the current audio resource. This includes filler silence packets + * that have been played when the resource was buffering. + */ + playbackDuration: number; + + /** + * The resource that is being played. + */ + resource: AudioResource; + + status: AudioPlayerStatus.Playing; +} + +/** + * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done + * automatically by the AudioPlayer itself if there are no available subscribers. + */ +export interface AudioPlayerPausedState { + onStreamError: (error: Error) => void; + /** + * The playback duration in milliseconds of the current audio resource. This includes filler silence packets + * that have been played when the resource was buffering. + */ + playbackDuration: number; + + /** + * The current resource of the audio player. + */ + resource: AudioResource; + + /** + * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing. + */ + silencePacketsRemaining: number; + + status: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused; +} + +/** + * The various states that the player can be in. + */ +export type AudioPlayerState = AudioPlayerBufferingState | AudioPlayerIdleState | AudioPlayerPausedState | AudioPlayerPlayingState; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface AudioPlayer extends EventEmitter { + /** + * Emitted when there is an error emitted from the audio resource played by the audio player + * + * @eventProperty + */ + on(event: 'error', listener: (error: AudioPlayerError) => void): this; + /** + * Emitted debugging information about the audio player + * + * @eventProperty + */ + on(event: 'debug', listener: (message: string) => void): this; + /** + * Emitted when the state of the audio player changes + * + * @eventProperty + */ + on(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this; + /** + * Emitted when the audio player is subscribed to a voice connection + * + * @eventProperty + */ + on(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this; + /** + * Emitted when the status of state changes to a specific status + * + * @eventProperty + */ + on(event: Event, listener: (oldState: AudioPlayerState, newState: AudioPlayerState & { status: Event }) => void): this; +} + +/** + * Stringifies an AudioPlayerState instance. + * + * @param state - The state to stringify + */ +function stringifyState(state: AudioPlayerState) { + return JSON.stringify({ + ...state, + resource: Reflect.has(state, 'resource'), + stepTimeout: Reflect.has(state, 'stepTimeout') + }); +} + +/** + * Used to play audio resources (i.e. tracks, streams) to voice connections. + * + * @remarks + * Audio players are designed to be re-used - even if a resource has finished playing, the player itself + * can still be used. + * + * The AudioPlayer drives the timing of playback, and therefore is unaffected by voice connections + * becoming unavailable. Its behavior in these scenarios can be configured. + */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class AudioPlayer extends EventEmitter { + /** + * The state that the AudioPlayer is in. + */ + private _state: AudioPlayerState; + + /** + * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio + * to the streams in this list. + */ + private readonly subscribers: PlayerSubscription[] = []; + + /** + * The behavior that the player should follow when it enters certain situations. + */ + private readonly behaviors: { + maxMissedFrames: number; + noSubscriber: NoSubscriberBehavior; + }; + + /** + * The debug logger function, if debugging is enabled. + */ + private readonly debug: ((message: string) => void) | null; + + /** + * Creates a new AudioPlayer. + */ + public constructor(options: CreateAudioPlayerOptions = {}) { + super(); + this._state = { status: AudioPlayerStatus.Idle }; + this.behaviors = { + noSubscriber: NoSubscriberBehavior.Pause, + maxMissedFrames: 5, + ...options.behaviors + }; + this.debug = options.debug === false ? null : (message: string) => this.emit('debug', message); + } + + /** + * A list of subscribed voice connections that can currently receive audio to play. + */ + public get playable() { + return this.subscribers.filter(({ connection }) => connection.state.status === VoiceConnectionStatus.Ready).map(({ connection }) => connection); + } + + /** + * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed, + * then the existing subscription is used. + * + * @remarks + * This method should not be directly called. Instead, use VoiceConnection#subscribe. + * @param connection - The connection to subscribe + * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription + */ + // @ts-ignore + private subscribe(connection: VoiceConnection) { + const existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection); + if (!existingSubscription) { + const subscription = new PlayerSubscription(connection, this); + this.subscribers.push(subscription); + setImmediate(() => this.emit('subscribe', subscription)); + return subscription; + } + + return existingSubscription; + } + + /** + * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player. + * + * @remarks + * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe. + * @param subscription - The subscription to remove + * @returns Whether or not the subscription existed on the player and was removed + */ + // @ts-ignore + private unsubscribe(subscription: PlayerSubscription) { + const index = this.subscribers.indexOf(subscription); + const exists = index !== -1; + if (exists) { + this.subscribers.splice(index, 1); + subscription.connection.setSpeaking(false); + this.emit('unsubscribe', subscription); + } + + return exists; + } + + /** + * The state that the player is in. + */ + public get state() { + return this._state; + } + + /** + * Sets a new state for the player, performing clean-up operations where necessary. + */ + public set state(newState: AudioPlayerState) { + const oldState = this._state; + const newResource = Reflect.get(newState, 'resource') as AudioResource | undefined; + + if (oldState.status !== AudioPlayerStatus.Idle && oldState.resource !== newResource) { + oldState.resource.playStream.on('error', noop); + oldState.resource.playStream.off('error', oldState.onStreamError); + oldState.resource.audioPlayer = undefined; + oldState.resource.playStream.destroy(); + oldState.resource.playStream.read(); // required to ensure buffered data is drained, prevents memory leak + } + + // When leaving the Buffering state (or buffering a new resource), then remove the event listeners from it + if (oldState.status === AudioPlayerStatus.Buffering && (newState.status !== AudioPlayerStatus.Buffering || newState.resource !== oldState.resource)) { + oldState.resource.playStream.off('end', oldState.onFailureCallback); + oldState.resource.playStream.off('close', oldState.onFailureCallback); + oldState.resource.playStream.off('finish', oldState.onFailureCallback); + oldState.resource.playStream.off('readable', oldState.onReadableCallback); + } + + // transitioning into an idle should ensure that connections stop speaking + if (newState.status === AudioPlayerStatus.Idle) { + this._signalStopSpeaking(); + deleteAudioPlayer(this); + } + + // attach to the global audio player timer + if (newResource) { + addAudioPlayer(this); + } + + // playing -> playing state changes should still transition if a resource changed (seems like it would be useful!) + const didChangeResources = oldState.status !== AudioPlayerStatus.Idle && newState.status === AudioPlayerStatus.Playing && oldState.resource !== newState.resource; + + this._state = newState; + + this.emit('stateChange', oldState, this._state); + if (oldState.status !== newState.status || didChangeResources) { + this.emit(newState.status, oldState, this._state as unsafe); + } + + this.debug?.(`state change:\nfrom ${stringifyState(oldState)}\nto ${stringifyState(newState)}`); + } + + /** + * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed + * (it cannot be reused, even in another player) and is replaced with the new resource. + * + * @remarks + * The player will transition to the Playing state once playback begins, and will return to the Idle state once + * playback is ended. + * + * If the player was previously playing a resource and this method is called, the player will not transition to the + * Idle state during the swap over. + * @param resource - The resource to play + * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player + */ + public play(resource: AudioResource) { + if (resource.ended) { + throw new Error('Cannot play a resource that has already ended.'); + } + + if (resource.audioPlayer) { + if (resource.audioPlayer === this) { + return; + } + + throw new Error('Resource is already being played by another audio player.'); + } + + resource.audioPlayer = this; + + // Attach error listeners to the stream that will propagate the error and then return to the Idle + // state if the resource is still being used. + const onStreamError = (error: Error) => { + if (this.state.status !== AudioPlayerStatus.Idle) { + this.emit('error', new AudioPlayerError(error, this.state.resource)); + } + + if (this.state.status !== AudioPlayerStatus.Idle && this.state.resource === resource) { + this.state = { + status: AudioPlayerStatus.Idle + }; + } + }; + + resource.playStream.once('error', onStreamError); + + if (resource.started) { + this.state = { + status: AudioPlayerStatus.Playing, + missedFrames: 0, + playbackDuration: 0, + resource, + onStreamError + }; + } else { + const onReadableCallback = () => { + if (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) { + this.state = { + status: AudioPlayerStatus.Playing, + missedFrames: 0, + playbackDuration: 0, + resource, + onStreamError + }; + } + }; + + const onFailureCallback = () => { + if (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) { + this.state = { + status: AudioPlayerStatus.Idle + }; + } + }; + + resource.playStream.once('readable', onReadableCallback); + + resource.playStream.once('end', onFailureCallback); + resource.playStream.once('close', onFailureCallback); + resource.playStream.once('finish', onFailureCallback); + + this.state = { + status: AudioPlayerStatus.Buffering, + resource, + onReadableCallback, + onFailureCallback, + onStreamError + }; + } + } + + /** + * Pauses playback of the current resource, if any. + * + * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches + * @returns `true` if the player was successfully paused, otherwise `false` + */ + public pause(interpolateSilence = true) { + if (this.state.status !== AudioPlayerStatus.Playing) return false; + this.state = { + ...this.state, + status: AudioPlayerStatus.Paused, + silencePacketsRemaining: interpolateSilence ? 5 : 0 + }; + return true; + } + + /** + * Unpauses playback of the current resource, if any. + * + * @returns `true` if the player was successfully unpaused, otherwise `false` + */ + public unpause() { + if (this.state.status !== AudioPlayerStatus.Paused) return false; + this.state = { + ...this.state, + status: AudioPlayerStatus.Playing, + missedFrames: 0 + }; + return true; + } + + /** + * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state, + * or remain in its current state until the silence padding frames of the resource have been played. + * + * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames + * @returns `true` if the player will come to a stop, otherwise `false` + */ + public stop(force = false) { + if (this.state.status === AudioPlayerStatus.Idle) return false; + if (force || this.state.resource.silencePaddingFrames === 0) { + this.state = { + status: AudioPlayerStatus.Idle + }; + } else if (this.state.resource.silenceRemaining === -1) { + this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames; + } + + return true; + } + + /** + * Checks whether the underlying resource (if any) is playable (readable) + * + * @returns `true` if the resource is playable, otherwise `false` + */ + public checkPlayable() { + const state = this._state; + if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return false; + + // If the stream has been destroyed or is no longer readable, then transition to the Idle state. + if (!state.resource.readable) { + this.state = { + status: AudioPlayerStatus.Idle + }; + return false; + } + + return true; + } + + /** + * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered + * by the active connections of this audio player. + */ + // @ts-ignore + private _stepDispatch() { + const state = this._state; + + // Guard against the Idle state + if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return; + + // Dispatch any audio packets that were prepared in the previous cycle + for (const connection of this.playable) { + connection.dispatchAudio(); + } + } + + /** + * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the + * underlying resource of the stream, and then has all the active connections of the audio player prepare it + * (encrypt it, append header data) so that it is ready to play at the start of the next cycle. + */ + // @ts-ignore + private _stepPrepare() { + const state = this._state; + + // Guard against the Idle state + if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return; + + // List of connections that can receive the packet + const playable = this.playable; + + /* If the player was previously in the AutoPaused state, check to see whether there are newly available + connections, allowing us to transition out of the AutoPaused state back into the Playing state */ + if (state.status === AudioPlayerStatus.AutoPaused && playable.length > 0) { + this.state = { + ...state, + status: AudioPlayerStatus.Playing, + missedFrames: 0 + }; + } + + /* If the player is (auto)paused, check to see whether silence packets should be played and + set a timeout to begin the next cycle, ending the current cycle here. */ + if (state.status === AudioPlayerStatus.Paused || state.status === AudioPlayerStatus.AutoPaused) { + if (state.silencePacketsRemaining > 0) { + state.silencePacketsRemaining--; + this._preparePacket(SILENCE_FRAME, playable, state); + if (state.silencePacketsRemaining === 0) { + this._signalStopSpeaking(); + } + } + + return; + } + + // If there are no available connections in this cycle, observe the configured "no subscriber" behavior. + if (playable.length === 0) { + if (this.behaviors.noSubscriber === NoSubscriberBehavior.Pause) { + this.state = { + ...state, + status: AudioPlayerStatus.AutoPaused, + silencePacketsRemaining: 5 + }; + return; + } else if (this.behaviors.noSubscriber === NoSubscriberBehavior.Stop) { + this.stop(true); + } + } + + /** + * Attempt to read an Opus packet from the resource. If there isn't an available packet, + * play a silence packet. If there are 5 consecutive cycles with failed reads, then the + * playback will end. + */ + const packet: Buffer | null = state.resource.read(); + + if (state.status === AudioPlayerStatus.Playing) { + if (packet) { + this._preparePacket(packet, playable, state); + state.missedFrames = 0; + } else { + this._preparePacket(SILENCE_FRAME, playable, state); + state.missedFrames++; + if (state.missedFrames >= this.behaviors.maxMissedFrames) { + this.stop(); + } + } + } + } + + /** + * Signals to all the subscribed connections that they should send a packet to Discord indicating + * they are no longer speaking. Called once playback of a resource ends. + */ + private _signalStopSpeaking() { + for (const { connection } of this.subscribers) { + connection.setSpeaking(false); + } + } + + /** + * Instructs the given connections to each prepare this packet to be played at the start of the + * next cycle. + * + * @param packet - The Opus packet to be prepared by each receiver + * @param receivers - The connections that should play this packet + */ + private _preparePacket(packet: Buffer, receivers: VoiceConnection[], state: AudioPlayerPausedState | AudioPlayerPlayingState) { + state.playbackDuration += 20; + for (const connection of receivers) { + connection.prepareAudioPacket(packet); + } + } +} + +/** + * Creates a new AudioPlayer to be used. + */ +export function createAudioPlayer(options?: CreateAudioPlayerOptions) { + return new AudioPlayer(options); +} diff --git a/packages/discord-voip/src/audio/AudioPlayerError.ts b/packages/discord-voip/src/audio/AudioPlayerError.ts new file mode 100644 index 0000000000..ab9ca8abf1 --- /dev/null +++ b/packages/discord-voip/src/audio/AudioPlayerError.ts @@ -0,0 +1,22 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import type { AudioResource } from './AudioResource'; + +/** + * An error emitted by an AudioPlayer. Contains an attached resource to aid with + * debugging and identifying where the error came from. + */ +export class AudioPlayerError extends Error { + /** + * The resource associated with the audio player at the time the error was thrown. + */ + public readonly resource: AudioResource; + + public constructor(error: Error, resource: AudioResource) { + super(error.message); + this.resource = resource; + this.name = error.name; + this.stack = error.stack!; + } +} diff --git a/packages/discord-voip/src/audio/AudioResource.ts b/packages/discord-voip/src/audio/AudioResource.ts new file mode 100644 index 0000000000..f4ca9477fa --- /dev/null +++ b/packages/discord-voip/src/audio/AudioResource.ts @@ -0,0 +1,272 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import type { Buffer } from 'node:buffer'; +import { pipeline, type Readable } from 'node:stream'; +import prism from 'prism-media'; +import { noop } from '../util/util'; +import { SILENCE_FRAME, type AudioPlayer } from './AudioPlayer'; +import { findPipeline, StreamType, TransformerType, type Edge } from './TransformerGraph'; + +/** + * Options that are set when creating a new audio resource. + * + * @typeParam Metadata - the type for the metadata (if any) of the audio resource + */ +export interface CreateAudioResourceOptions { + /** + * Whether or not inline volume should be enabled. If enabled, you will be able to change the volume + * of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`. + */ + inlineVolume?: boolean; + + /** + * The type of the input stream. Defaults to `StreamType.Arbitrary`. + */ + inputType?: StreamType; + + /** + * Optional metadata that can be attached to the resource (e.g. track title, random id). + * This is useful for identification purposes when the resource is passed around in events. + * See {@link AudioResource.metadata} + */ + metadata?: Metadata; + + /** + * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. + * Defaults to 5. + */ + silencePaddingFrames?: number; +} + +/** + * Represents an audio resource that can be played by an audio player. + * + * @typeParam Metadata - the type for the metadata (if any) of the audio resource + */ +export class AudioResource { + /** + * An object-mode Readable stream that emits Opus packets. This is what is played by audio players. + */ + public readonly playStream: Readable; + + /** + * The pipeline used to convert the input stream into a playable format. For example, this may + * contain an FFmpeg component for arbitrary inputs, and it may contain a VolumeTransformer component + * for resources with inline volume transformation enabled. + */ + public readonly edges: readonly Edge[]; + + /** + * Optional metadata that can be used to identify the resource. + */ + public metadata: Metadata; + + /** + * If the resource was created with inline volume transformation enabled, then this will be a + * prism-media VolumeTransformer. You can use this to alter the volume of the stream. + */ + public readonly volume?: prism.VolumeTransformer; + + /** + * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder. + * You can use this to control settings such as bitrate, FEC, PLP. + */ + public readonly encoder?: prism.opus.Encoder; + + /** + * The audio player that the resource is subscribed to, if any. + */ + public audioPlayer?: AudioPlayer | undefined; + + /** + * The playback duration of this audio resource, given in milliseconds. + */ + public playbackDuration = 0; + + /** + * Whether or not the stream for this resource has started (data has become readable) + */ + public started = false; + + /** + * The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches. + */ + public readonly silencePaddingFrames: number; + + /** + * The number of remaining silence frames to play. If -1, the frames have not yet started playing. + */ + public silenceRemaining = -1; + + public constructor(edges: readonly Edge[], streams: readonly Readable[], metadata: Metadata, silencePaddingFrames: number) { + this.edges = edges; + this.playStream = streams.length > 1 ? (pipeline(streams, noop) as unknown as Readable) : streams[0]!; + this.metadata = metadata; + this.silencePaddingFrames = silencePaddingFrames; + + for (const stream of streams) { + if (stream instanceof prism.VolumeTransformer) { + this.volume = stream; + } else if (stream instanceof prism.opus.Encoder) { + this.encoder = stream; + } + } + + this.playStream.once('readable', () => (this.started = true)); + } + + /** + * Whether this resource is readable. If the underlying resource is no longer readable, this will still return true + * while there are silence padding frames left to play. + */ + public get readable() { + if (this.silenceRemaining === 0) return false; + const real = this.playStream.readable; + if (!real) { + if (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames; + return this.silenceRemaining !== 0; + } + + return real; + } + + /** + * Whether this resource has ended or not. + */ + public get ended() { + return this.playStream.readableEnded || this.playStream.destroyed || this.silenceRemaining === 0; + } + + /** + * Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration + * is incremented. + * + * @remarks + * It is advisable to check that the playStream is readable before calling this method. While no runtime + * errors will be thrown, you should check that the resource is still available before attempting to + * read from it. + * @internal + */ + public read(): Buffer | null { + if (this.silenceRemaining === 0) { + return null; + } else if (this.silenceRemaining > 0) { + this.silenceRemaining--; + return SILENCE_FRAME; + } + + const packet = this.playStream.read() as Buffer | null; + if (packet) { + this.playbackDuration += 20; + } + + return packet; + } +} + +/** + * Ensures that a path contains at least one volume transforming component. + * + * @param path - The path to validate constraints on + */ +export const VOLUME_CONSTRAINT = (path: Edge[]) => path.some((edge) => edge.type === TransformerType.InlineVolume); + +export const NO_CONSTRAINT = () => true; + +/** + * Tries to infer the type of a stream to aid with transcoder pipelining. + * + * @param stream - The stream to infer the type of + */ +export function inferStreamType(stream: Readable): { + hasVolume: boolean; + streamType: StreamType; +} { + if (stream instanceof prism.opus.Encoder) { + return { streamType: StreamType.Opus, hasVolume: false }; + } else if (stream instanceof prism.opus.Decoder) { + return { streamType: StreamType.Raw, hasVolume: false }; + } else if (stream instanceof prism.VolumeTransformer) { + return { streamType: StreamType.Raw, hasVolume: true }; + } else if (stream instanceof prism.opus.OggDemuxer) { + return { streamType: StreamType.Opus, hasVolume: false }; + } else if (stream instanceof prism.opus.WebmDemuxer) { + return { streamType: StreamType.Opus, hasVolume: false }; + } + + return { streamType: StreamType.Arbitrary, hasVolume: false }; +} + +/** + * Creates an audio resource that can be played by audio players. + * + * @remarks + * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. + * + * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created + * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, + * Opus transcoders, and Ogg/WebM demuxers. + * @param input - The resource to play + * @param options - Configurable options for creating the resource + * @typeParam Metadata - the type for the metadata (if any) of the audio resource + */ +export function createAudioResource( + input: Readable | string, + options: CreateAudioResourceOptions & Pick : Required>, 'metadata'> +): AudioResource; + +/** + * Creates an audio resource that can be played by audio players. + * + * @remarks + * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. + * + * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created + * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, + * Opus transcoders, and Ogg/WebM demuxers. + * @param input - The resource to play + * @param options - Configurable options for creating the resource + * @typeParam Metadata - the type for the metadata (if any) of the audio resource + */ +export function createAudioResource(input: Readable | string, options?: Omit, 'metadata'>): AudioResource; + +/** + * Creates an audio resource that can be played by audio players. + * + * @remarks + * If the input is given as a string, then the inputType option will be overridden and FFmpeg will be used. + * + * If the input is not in the correct format, then a pipeline of transcoders and transformers will be created + * to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg, + * Opus transcoders, and Ogg/WebM demuxers. + * @param input - The resource to play + * @param options - Configurable options for creating the resource + * @typeParam Metadata - the type for the metadata (if any) of the audio resource + */ +export function createAudioResource(input: Readable | string, options: CreateAudioResourceOptions = {}): AudioResource { + let inputType = options.inputType; + let needsInlineVolume = Boolean(options.inlineVolume); + + // string inputs can only be used with FFmpeg + if (typeof input === 'string') { + inputType = StreamType.Arbitrary; + } else if (inputType === undefined) { + const analysis = inferStreamType(input); + inputType = analysis.streamType; + needsInlineVolume = needsInlineVolume && !analysis.hasVolume; + } + + const transformerPipeline = findPipeline(inputType, needsInlineVolume ? VOLUME_CONSTRAINT : NO_CONSTRAINT); + + if (transformerPipeline.length === 0) { + if (typeof input === 'string') throw new Error(`Invalid pipeline constructed for string resource '${input}'`); + // No adjustments required + return new AudioResource([], [input], (options.metadata ?? null) as Metadata, options.silencePaddingFrames ?? 5); + } + + const streams = transformerPipeline.map((edge) => edge.transformer(input)); + if (typeof input !== 'string') streams.unshift(input); + + return new AudioResource(transformerPipeline, streams, (options.metadata ?? null) as Metadata, options.silencePaddingFrames ?? 5); +} diff --git a/packages/discord-voip/src/audio/PlayerSubscription.ts b/packages/discord-voip/src/audio/PlayerSubscription.ts new file mode 100644 index 0000000000..73e00b841d --- /dev/null +++ b/packages/discord-voip/src/audio/PlayerSubscription.ts @@ -0,0 +1,36 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +/* eslint-disable @typescript-eslint/dot-notation */ +import type { VoiceConnection } from '../VoiceConnection'; +import type { AudioPlayer } from './AudioPlayer'; + +/** + * Represents a subscription of a voice connection to an audio player, allowing + * the audio player to play audio on the voice connection. + */ +export class PlayerSubscription { + /** + * The voice connection of this subscription. + */ + public readonly connection: VoiceConnection; + + /** + * The audio player of this subscription. + */ + public readonly player: AudioPlayer; + + public constructor(connection: VoiceConnection, player: AudioPlayer) { + this.connection = connection; + this.player = player; + } + + /** + * Unsubscribes the connection from the audio player, meaning that the + * audio player cannot stream audio to it until a new subscription is made. + */ + public unsubscribe() { + this.connection['onSubscriptionRemoved'](this); + this.player['unsubscribe'](this); + } +} diff --git a/packages/discord-voip/src/audio/TransformerGraph.ts b/packages/discord-voip/src/audio/TransformerGraph.ts new file mode 100644 index 0000000000..4772ade2f9 --- /dev/null +++ b/packages/discord-voip/src/audio/TransformerGraph.ts @@ -0,0 +1,271 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import type { Readable } from 'node:stream'; +import { OpusEncoder, OpusDecoder, OggDemuxer, WebmDemuxer } from '@discord-player/opus'; +import { createFFmpegArgs, FFmpeg } from '@discord-player/ffmpeg'; +import { VolumeTransformer } from '@discord-player/equalizer'; + +const FFMPEG_PCM_ARGUMENTS = createFFmpegArgs({ + analyzeduration: '0', + loglevel: '0', + f: 's16le', + ar: '48000', + ac: '2' +}); + +const FFMPEG_OPUS_ARGUMENTS = createFFmpegArgs({ + analyzeduration: '0', + loglevel: '0', + acodec: 'libopus', + f: 'opus', + ar: '48000', + ac: '2' +}); + +/** + * The different types of stream that can exist within the pipeline. + */ +export enum StreamType { + /** + * The type of the stream at this point is unknown. + */ + Arbitrary = 'arbitrary', + /** + * The stream at this point is Opus audio encoded in an Ogg wrapper. + */ + OggOpus = 'ogg/opus', + /** + * The stream at this point is Opus audio, and the stream is in object-mode. This is ready to play. + */ + Opus = 'opus', + /** + * The stream at this point is s16le PCM. + */ + Raw = 'raw', + /** + * The stream at this point is Opus audio encoded in a WebM wrapper. + */ + WebmOpus = 'webm/opus' +} + +/** + * The different types of transformers that can exist within the pipeline. + */ +export enum TransformerType { + FFmpegOgg = 'ffmpeg ogg', + FFmpegPCM = 'ffmpeg pcm', + InlineVolume = 'volume transformer', + OggOpusDemuxer = 'ogg/opus demuxer', + OpusDecoder = 'opus decoder', + OpusEncoder = 'opus encoder', + WebmOpusDemuxer = 'webm/opus demuxer' +} + +/** + * Represents a pathway from one stream type to another using a transformer. + */ +export interface Edge { + cost: number; + from: Node; + to: Node; + transformer(input: Readable | string): Readable; + type: TransformerType; +} + +/** + * Represents a type of stream within the graph, e.g. an Opus stream, or a stream of raw audio. + */ +export class Node { + /** + * The outbound edges from this node. + */ + public readonly edges: Edge[] = []; + + /** + * The type of stream for this node. + */ + public readonly type: StreamType; + + public constructor(type: StreamType) { + this.type = type; + } + + /** + * Creates an outbound edge from this node. + * + * @param edge - The edge to create + */ + public addEdge(edge: Omit) { + this.edges.push({ ...edge, from: this }); + } +} + +// Create a node for each stream type +let NODES: Map | null = null; + +function canEnableFFmpegOptimizations(): boolean { + return FFmpeg.resolveSafe()?.command.includes('--enable-libopus') === true; +} + +/** + * Gets a node from its stream type. + * + * @param type - The stream type of the target node + */ +export function getNode(type: StreamType) { + const node = (NODES ??= initializeNodes()).get(type); + if (!node) throw new Error(`Node type '${type}' does not exist!`); + return node; +} + +function initializeNodes(): Map { + const nodes = new Map(); + for (const streamType of Object.values(StreamType)) { + nodes.set(streamType, new Node(streamType)); + } + + nodes.get(StreamType.Raw)!.addEdge({ + type: TransformerType.OpusEncoder, + to: nodes.get(StreamType.Opus)!, + cost: 1.5, + transformer: () => new OpusEncoder({ rate: 48_000, channels: 2, frameSize: 960 }) + }); + + nodes.get(StreamType.Opus)!.addEdge({ + type: TransformerType.OpusDecoder, + to: nodes.get(StreamType.Raw)!, + cost: 1.5, + transformer: () => new OpusDecoder({ rate: 48_000, channels: 2, frameSize: 960 }) + }); + + nodes.get(StreamType.OggOpus)!.addEdge({ + type: TransformerType.OggOpusDemuxer, + to: nodes.get(StreamType.Opus)!, + cost: 1, + transformer: () => new OggDemuxer() + }); + + nodes.get(StreamType.WebmOpus)!.addEdge({ + type: TransformerType.WebmOpusDemuxer, + to: nodes.get(StreamType.Opus)!, + cost: 1, + transformer: () => new WebmDemuxer() + }); + + const FFMPEG_PCM_EDGE: Omit = { + type: TransformerType.FFmpegPCM, + to: nodes.get(StreamType.Raw)!, + cost: 2, + transformer: (input) => + new FFmpeg({ + args: ['-i', typeof input === 'string' ? input : '-', ...FFMPEG_PCM_ARGUMENTS] + }) + }; + + nodes.get(StreamType.Arbitrary)!.addEdge(FFMPEG_PCM_EDGE); + nodes.get(StreamType.OggOpus)!.addEdge(FFMPEG_PCM_EDGE); + nodes.get(StreamType.WebmOpus)!.addEdge(FFMPEG_PCM_EDGE); + + nodes.get(StreamType.Raw)!.addEdge({ + type: TransformerType.InlineVolume, + to: nodes.get(StreamType.Raw)!, + cost: 0.5, + transformer: () => new VolumeTransformer({ type: 's16le' }) + }); + + if (canEnableFFmpegOptimizations()) { + const FFMPEG_OGG_EDGE: Omit = { + type: TransformerType.FFmpegOgg, + to: nodes.get(StreamType.OggOpus)!, + cost: 2, + transformer: (input) => + new FFmpeg({ + args: ['-i', typeof input === 'string' ? input : '-', ...FFMPEG_OPUS_ARGUMENTS] + }) + }; + nodes.get(StreamType.Arbitrary)!.addEdge(FFMPEG_OGG_EDGE); + // Include Ogg and WebM as well in case they have different sampling rates or are mono instead of stereo + // at the moment, this will not do anything. However, if/when detection for correct Opus headers is + // implemented, this will help inform the voice engine that it is able to transcode the audio. + nodes.get(StreamType.OggOpus)!.addEdge(FFMPEG_OGG_EDGE); + nodes.get(StreamType.WebmOpus)!.addEdge(FFMPEG_OGG_EDGE); + } + + return nodes; +} + +/** + * Represents a step in the path from node A to node B. + */ +interface Step { + /** + * The cost of the steps after this step. + */ + cost: number; + + /** + * The edge associated with this step. + */ + edge?: Edge; + + /** + * The next step. + */ + next?: Step; +} + +/** + * Finds the shortest cost path from node A to node B. + * + * @param from - The start node + * @param constraints - Extra validation for a potential solution. Takes a path, returns true if the path is valid + * @param goal - The target node + * @param path - The running path + * @param depth - The number of remaining recursions + */ +function findPath(from: Node, constraints: (path: Edge[]) => boolean, goal = getNode(StreamType.Opus), path: Edge[] = [], depth = 5): Step { + if (from === goal && constraints(path)) { + return { cost: 0 }; + } else if (depth === 0) { + return { cost: Number.POSITIVE_INFINITY }; + } + + let currentBest: Step | undefined; + for (const edge of from.edges) { + if (currentBest && edge.cost > currentBest.cost) continue; + const next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1); + const cost = edge.cost + next.cost; + if (!currentBest || cost < currentBest.cost) { + currentBest = { cost, edge, next }; + } + } + + return currentBest ?? { cost: Number.POSITIVE_INFINITY }; +} + +/** + * Takes the solution from findPath and assembles it into a list of edges. + * + * @param step - The first step of the path + */ +function constructPipeline(step: Step) { + const edges = []; + let current: Step | undefined = step; + while (current?.edge) { + edges.push(current.edge); + current = current.next; + } + + return edges; +} + +/** + * Finds the lowest-cost pipeline to convert the input stream type into an Opus stream. + * + * @param from - The stream type to start from + * @param constraint - Extra constraints that may be imposed on potential solution + */ +export function findPipeline(from: StreamType, constraint: (path: Edge[]) => boolean) { + return constructPipeline(findPath(getNode(from), constraint)); +} diff --git a/packages/discord-voip/src/audio/index.ts b/packages/discord-voip/src/audio/index.ts new file mode 100644 index 0000000000..7bcb62dfb5 --- /dev/null +++ b/packages/discord-voip/src/audio/index.ts @@ -0,0 +1,23 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +export { + AudioPlayer, + AudioPlayerStatus, + type AudioPlayerState, + NoSubscriberBehavior, + createAudioPlayer, + type AudioPlayerBufferingState, + type AudioPlayerIdleState, + type AudioPlayerPausedState, + type AudioPlayerPlayingState, + type CreateAudioPlayerOptions +} from './AudioPlayer'; + +export { AudioPlayerError } from './AudioPlayerError'; + +export { AudioResource, type CreateAudioResourceOptions, createAudioResource } from './AudioResource'; + +export { PlayerSubscription } from './PlayerSubscription'; + +export { StreamType } from './TransformerGraph'; diff --git a/packages/discord-voip/src/common/types.ts b/packages/discord-voip/src/common/types.ts new file mode 100644 index 0000000000..d29b6e7ae7 --- /dev/null +++ b/packages/discord-voip/src/common/types.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type unsafe = any; diff --git a/packages/discord-voip/src/index.ts b/packages/discord-voip/src/index.ts new file mode 100644 index 0000000000..0b97f3d223 --- /dev/null +++ b/packages/discord-voip/src/index.ts @@ -0,0 +1,23 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +export * from './joinVoiceChannel'; +export * from './audio/index'; +export * from './util/index'; + +export { + VoiceConnection, + type VoiceConnectionState, + VoiceConnectionStatus, + type VoiceConnectionConnectingState, + type VoiceConnectionDestroyedState, + type VoiceConnectionDisconnectedState, + type VoiceConnectionDisconnectedBaseState, + type VoiceConnectionDisconnectedOtherState, + type VoiceConnectionDisconnectedWebSocketState, + VoiceConnectionDisconnectReason, + type VoiceConnectionReadyState, + type VoiceConnectionSignallingState +} from './VoiceConnection'; + +export { type JoinConfig, getVoiceConnection, getVoiceConnections, getGroups } from './DataStore'; diff --git a/packages/discord-voip/src/joinVoiceChannel.ts b/packages/discord-voip/src/joinVoiceChannel.ts new file mode 100644 index 0000000000..6719f3a9fc --- /dev/null +++ b/packages/discord-voip/src/joinVoiceChannel.ts @@ -0,0 +1,68 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import type { JoinConfig } from './DataStore'; +import { createVoiceConnection } from './VoiceConnection'; +import type { DiscordGatewayAdapterCreator } from './util/adapter'; + +/** + * The options that can be given when creating a voice connection. + */ +export interface CreateVoiceConnectionOptions { + adapterCreator: DiscordGatewayAdapterCreator; + + /** + * If true, debug messages will be enabled for the voice connection and its + * related components. Defaults to false. + */ + debug?: boolean | undefined; +} + +/** + * The options that can be given when joining a voice channel. + */ +export interface JoinVoiceChannelOptions { + /** + * The id of the Discord voice channel to join. + */ + channelId: string; + + /** + * An optional group identifier for the voice connection. + */ + group?: string; + + /** + * The id of the guild that the voice channel belongs to. + */ + guildId: string; + + /** + * Whether to join the channel deafened (defaults to true) + */ + selfDeaf?: boolean; + + /** + * Whether to join the channel muted (defaults to true) + */ + selfMute?: boolean; +} + +/** + * Creates a VoiceConnection to a Discord voice channel. + * + * @param options - the options for joining the voice channel + */ +export function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions) { + const joinConfig: JoinConfig = { + selfDeaf: true, + selfMute: false, + group: 'default', + ...options + }; + + return createVoiceConnection(joinConfig, { + adapterCreator: options.adapterCreator, + debug: options.debug + }); +} diff --git a/packages/discord-voip/src/networking/Networking.ts b/packages/discord-voip/src/networking/Networking.ts new file mode 100644 index 0000000000..a9058609c4 --- /dev/null +++ b/packages/discord-voip/src/networking/Networking.ts @@ -0,0 +1,589 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +/* eslint-disable id-length */ +/* eslint-disable @typescript-eslint/unbound-method */ +import { Buffer } from 'node:buffer'; +import { EventEmitter } from 'node:events'; +import { VoiceOpcodes } from 'discord-api-types/voice/v4'; +import type { CloseEvent } from 'ws'; +import * as secretbox from '../util/Secretbox'; +import { noop } from '../util/util'; +import { VoiceUDPSocket } from './VoiceUDPSocket'; +import { VoiceWebSocket } from './VoiceWebSocket'; +import { unsafe } from '../common/types'; + +// The number of audio channels required by Discord +const CHANNELS = 2; +const TIMESTAMP_INC = (48_000 / 100) * CHANNELS; +const MAX_NONCE_SIZE = 2 ** 32 - 1; + +export const SUPPORTED_ENCRYPTION_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305']; + +/** + * The different statuses that a networking instance can hold. The order + * of the states between OpeningWs and Ready is chronological (first the + * instance enters OpeningWs, then it enters Identifying etc.) + */ +export enum NetworkingStatusCode { + OpeningWs, + Identifying, + UdpHandshaking, + SelectingProtocol, + Ready, + Resuming, + Closed +} + +/** + * The initial Networking state. Instances will be in this state when a WebSocket connection to a Discord + * voice gateway is being opened. + */ +export interface NetworkingOpeningWsState { + code: NetworkingStatusCode.OpeningWs; + connectionOptions: ConnectionOptions; + ws: VoiceWebSocket; +} + +/** + * The state that a Networking instance will be in when it is attempting to authorize itself. + */ +export interface NetworkingIdentifyingState { + code: NetworkingStatusCode.Identifying; + connectionOptions: ConnectionOptions; + ws: VoiceWebSocket; +} + +/** + * The state that a Networking instance will be in when opening a UDP connection to the IP and port provided + * by Discord, as well as performing IP discovery. + */ +export interface NetworkingUdpHandshakingState { + code: NetworkingStatusCode.UdpHandshaking; + connectionData: Pick; + connectionOptions: ConnectionOptions; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; +} + +/** + * The state that a Networking instance will be in when selecting an encryption protocol for audio packets. + */ +export interface NetworkingSelectingProtocolState { + code: NetworkingStatusCode.SelectingProtocol; + connectionData: Pick; + connectionOptions: ConnectionOptions; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; +} + +/** + * The state that a Networking instance will be in when it has a fully established connection to a Discord + * voice server. + */ +export interface NetworkingReadyState { + code: NetworkingStatusCode.Ready; + connectionData: ConnectionData; + connectionOptions: ConnectionOptions; + preparedPacket?: Buffer | undefined; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; +} + +/** + * The state that a Networking instance will be in when its connection has been dropped unexpectedly, and it + * is attempting to resume an existing session. + */ +export interface NetworkingResumingState { + code: NetworkingStatusCode.Resuming; + connectionData: ConnectionData; + connectionOptions: ConnectionOptions; + preparedPacket?: Buffer | undefined; + udp: VoiceUDPSocket; + ws: VoiceWebSocket; +} + +/** + * The state that a Networking instance will be in when it has been destroyed. It cannot be recovered from this + * state. + */ +export interface NetworkingClosedState { + code: NetworkingStatusCode.Closed; +} + +/** + * The various states that a networking instance can be in. + */ +export type NetworkingState = + | NetworkingClosedState + | NetworkingIdentifyingState + | NetworkingOpeningWsState + | NetworkingReadyState + | NetworkingResumingState + | NetworkingSelectingProtocolState + | NetworkingUdpHandshakingState; + +/** + * Details required to connect to the Discord voice gateway. These details + * are first received on the main bot gateway, in the form of VOICE_SERVER_UPDATE + * and VOICE_STATE_UPDATE packets. + */ +interface ConnectionOptions { + endpoint: string; + serverId: string; + sessionId: string; + token: string; + userId: string; +} + +/** + * Information about the current connection, e.g. which encryption mode is to be used on + * the connection, timing information for playback of streams. + */ +export interface ConnectionData { + encryptionMode: string; + nonce: number; + nonceBuffer: Buffer; + packetsPlayed: number; + secretKey: Uint8Array; + sequence: number; + speaking: boolean; + ssrc: number; + timestamp: number; +} + +/** + * An empty buffer that is reused in packet encryption by many different networking instances. + */ +const nonce = Buffer.alloc(24); + +export interface Networking extends EventEmitter { + /** + * Debug event for Networking. + * + * @eventProperty + */ + on(event: 'debug', listener: (message: string) => void): this; + on(event: 'error', listener: (error: Error) => void): this; + on(event: 'stateChange', listener: (oldState: NetworkingState, newState: NetworkingState) => void): this; + on(event: 'close', listener: (code: number) => void): this; +} + +/** + * Stringifies a NetworkingState. + * + * @param state - The state to stringify + */ +function stringifyState(state: NetworkingState) { + return JSON.stringify({ + ...state, + ws: Reflect.has(state, 'ws'), + udp: Reflect.has(state, 'udp') + }); +} + +/** + * Chooses an encryption mode from a list of given options. Chooses the most preferred option. + * + * @param options - The available encryption options + */ +function chooseEncryptionMode(options: string[]): string { + const option = options.find((option) => SUPPORTED_ENCRYPTION_MODES.includes(option)); + if (!option) { + throw new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`); + } + + return option; +} + +/** + * Returns a random number that is in the range of n bits. + * + * @param numberOfBits - The number of bits + */ +function randomNBit(numberOfBits: number) { + return Math.floor(Math.random() * 2 ** numberOfBits); +} + +/** + * Manages the networking required to maintain a voice connection and dispatch audio packets + */ +export class Networking extends EventEmitter { + private _state: NetworkingState; + + /** + * The debug logger function, if debugging is enabled. + */ + private readonly debug: ((message: string) => void) | null; + + /** + * Creates a new Networking instance. + */ + public constructor(options: ConnectionOptions, debug: boolean) { + super(); + + this.onWsOpen = this.onWsOpen.bind(this); + this.onChildError = this.onChildError.bind(this); + this.onWsPacket = this.onWsPacket.bind(this); + this.onWsClose = this.onWsClose.bind(this); + this.onWsDebug = this.onWsDebug.bind(this); + this.onUdpDebug = this.onUdpDebug.bind(this); + this.onUdpClose = this.onUdpClose.bind(this); + + this.debug = debug ? (message: string) => this.emit('debug', message) : null; + + this._state = { + code: NetworkingStatusCode.OpeningWs, + ws: this.createWebSocket(options.endpoint), + connectionOptions: options + }; + } + + /** + * Destroys the Networking instance, transitioning it into the Closed state. + */ + public destroy() { + this.state = { + code: NetworkingStatusCode.Closed + }; + } + + /** + * The current state of the networking instance. + */ + public get state(): NetworkingState { + return this._state; + } + + /** + * Sets a new state for the networking instance, performing clean-up operations where necessary. + */ + public set state(newState: NetworkingState) { + const oldWs = Reflect.get(this._state, 'ws') as VoiceWebSocket | undefined; + const newWs = Reflect.get(newState, 'ws') as VoiceWebSocket | undefined; + if (oldWs && oldWs !== newWs) { + // The old WebSocket is being freed - remove all handlers from it + oldWs.off('debug', this.onWsDebug); + oldWs.on('error', noop); + oldWs.off('error', this.onChildError); + oldWs.off('open', this.onWsOpen); + oldWs.off('packet', this.onWsPacket); + oldWs.off('close', this.onWsClose); + oldWs.destroy(); + } + + const oldUdp = Reflect.get(this._state, 'udp') as VoiceUDPSocket | undefined; + const newUdp = Reflect.get(newState, 'udp') as VoiceUDPSocket | undefined; + + if (oldUdp && oldUdp !== newUdp) { + oldUdp.on('error', noop); + oldUdp.off('error', this.onChildError); + oldUdp.off('close', this.onUdpClose); + oldUdp.off('debug', this.onUdpDebug); + oldUdp.destroy(); + } + + const oldState = this._state; + this._state = newState; + this.emit('stateChange', oldState, newState); + + this.debug?.(`state change:\nfrom ${stringifyState(oldState)}\nto ${stringifyState(newState)}`); + } + + /** + * Creates a new WebSocket to a Discord Voice gateway. + * + * @param endpoint - The endpoint to connect to + */ + private createWebSocket(endpoint: string) { + const ws = new VoiceWebSocket(`wss://${endpoint}?v=4`, Boolean(this.debug)); + + ws.on('error', this.onChildError); + ws.once('open', this.onWsOpen); + ws.on('packet', this.onWsPacket); + ws.once('close', this.onWsClose); + ws.on('debug', this.onWsDebug); + + return ws; + } + + /** + * Propagates errors from the children VoiceWebSocket and VoiceUDPSocket. + * + * @param error - The error that was emitted by a child + */ + private onChildError(error: Error) { + this.emit('error', error); + } + + /** + * Called when the WebSocket opens. Depending on the state that the instance is in, + * it will either identify with a new session, or it will attempt to resume an existing session. + */ + private onWsOpen() { + if (this.state.code === NetworkingStatusCode.OpeningWs) { + const packet = { + op: VoiceOpcodes.Identify, + d: { + server_id: this.state.connectionOptions.serverId, + user_id: this.state.connectionOptions.userId, + session_id: this.state.connectionOptions.sessionId, + token: this.state.connectionOptions.token + } + }; + this.state.ws.sendPacket(packet); + this.state = { + ...this.state, + code: NetworkingStatusCode.Identifying + }; + } else if (this.state.code === NetworkingStatusCode.Resuming) { + const packet = { + op: VoiceOpcodes.Resume, + d: { + server_id: this.state.connectionOptions.serverId, + session_id: this.state.connectionOptions.sessionId, + token: this.state.connectionOptions.token + } + }; + this.state.ws.sendPacket(packet); + } + } + + /** + * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter), + * the instance will either attempt to resume, or enter the closed state and emit a 'close' event + * with the close code, allowing the user to decide whether or not they would like to reconnect. + * + * @param code - The close code + */ + private onWsClose({ code }: CloseEvent) { + const canResume = code === 4_015 || code < 4_000; + if (canResume && this.state.code === NetworkingStatusCode.Ready) { + this.state = { + ...this.state, + code: NetworkingStatusCode.Resuming, + ws: this.createWebSocket(this.state.connectionOptions.endpoint) + }; + } else if (this.state.code !== NetworkingStatusCode.Closed) { + this.destroy(); + this.emit('close', code); + } + } + + /** + * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord. + */ + private onUdpClose() { + if (this.state.code === NetworkingStatusCode.Ready) { + this.state = { + ...this.state, + code: NetworkingStatusCode.Resuming, + ws: this.createWebSocket(this.state.connectionOptions.endpoint) + }; + } + } + + /** + * Called when a packet is received on the connection's WebSocket. + * + * @param packet - The received packet + */ + private onWsPacket(packet: unsafe) { + if (packet.op === VoiceOpcodes.Hello && this.state.code !== NetworkingStatusCode.Closed) { + this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval); + } else if (packet.op === VoiceOpcodes.Ready && this.state.code === NetworkingStatusCode.Identifying) { + const { ip, port, ssrc, modes } = packet.d; + + const udp = new VoiceUDPSocket({ ip, port }); + udp.on('error', this.onChildError); + udp.on('debug', this.onUdpDebug); + udp.once('close', this.onUdpClose); + udp.performIPDiscovery(ssrc) + .then((localConfig) => { + if (this.state.code !== NetworkingStatusCode.UdpHandshaking) return; + this.state.ws.sendPacket({ + op: VoiceOpcodes.SelectProtocol, + d: { + protocol: 'udp', + data: { + address: localConfig.ip, + port: localConfig.port, + mode: chooseEncryptionMode(modes) + } + } + }); + this.state = { + ...this.state, + code: NetworkingStatusCode.SelectingProtocol + }; + }) + .catch((error: Error) => this.emit('error', error)); + + this.state = { + ...this.state, + code: NetworkingStatusCode.UdpHandshaking, + udp, + connectionData: { + ssrc + } + }; + } else if (packet.op === VoiceOpcodes.SessionDescription && this.state.code === NetworkingStatusCode.SelectingProtocol) { + const { mode: encryptionMode, secret_key: secretKey } = packet.d; + this.state = { + ...this.state, + code: NetworkingStatusCode.Ready, + connectionData: { + ...this.state.connectionData, + encryptionMode, + secretKey: new Uint8Array(secretKey), + sequence: randomNBit(16), + timestamp: randomNBit(32), + nonce: 0, + nonceBuffer: Buffer.alloc(24), + speaking: false, + packetsPlayed: 0 + } + }; + } else if (packet.op === VoiceOpcodes.Resumed && this.state.code === NetworkingStatusCode.Resuming) { + this.state = { + ...this.state, + code: NetworkingStatusCode.Ready + }; + this.state.connectionData.speaking = false; + } + } + + /** + * Propagates debug messages from the child WebSocket. + * + * @param message - The emitted debug message + */ + private onWsDebug(message: string) { + this.debug?.(`[WS] ${message}`); + } + + /** + * Propagates debug messages from the child UDPSocket. + * + * @param message - The emitted debug message + */ + private onUdpDebug(message: string) { + this.debug?.(`[UDP] ${message}`); + } + + /** + * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it. + * It will be stored within the instance, and can be played by dispatchAudio() + * + * @remarks + * Calling this method while there is already a prepared audio packet that has not yet been dispatched + * will overwrite the existing audio packet. This should be avoided. + * @param opusPacket - The Opus packet to encrypt + * @returns The audio packet that was prepared + */ + public prepareAudioPacket(opusPacket: Buffer) { + const state = this.state; + if (state.code !== NetworkingStatusCode.Ready) return; + state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData); + return state.preparedPacket; + } + + /** + * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet + * is consumed and cannot be dispatched again. + */ + public dispatchAudio() { + const state = this.state; + if (state.code !== NetworkingStatusCode.Ready) return false; + if (state.preparedPacket !== undefined) { + this.playAudioPacket(state.preparedPacket); + state.preparedPacket = undefined; + return true; + } + + return false; + } + + /** + * Plays an audio packet, updating timing metadata used for playback. + * + * @param audioPacket - The audio packet to play + */ + private playAudioPacket(audioPacket: Buffer) { + const state = this.state; + if (state.code !== NetworkingStatusCode.Ready) return; + const { connectionData } = state; + connectionData.packetsPlayed++; + connectionData.sequence++; + connectionData.timestamp += TIMESTAMP_INC; + if (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0; + if (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0; + this.setSpeaking(true); + state.udp.send(audioPacket); + } + + /** + * Sends a packet to the voice gateway indicating that the client has start/stopped sending + * audio. + * + * @param speaking - Whether or not the client should be shown as speaking + */ + public setSpeaking(speaking: boolean) { + const state = this.state; + if (state.code !== NetworkingStatusCode.Ready) return; + if (state.connectionData.speaking === speaking) return; + state.connectionData.speaking = speaking; + state.ws.sendPacket({ + op: VoiceOpcodes.Speaking, + d: { + speaking: speaking ? 1 : 0, + delay: 0, + ssrc: state.connectionData.ssrc + } + }); + } + + /** + * Creates a new audio packet from an Opus packet. This involves encrypting the packet, + * then prepending a header that includes metadata. + * + * @param opusPacket - The Opus packet to prepare + * @param connectionData - The current connection data of the instance + */ + private createAudioPacket(opusPacket: Buffer, connectionData: ConnectionData) { + const packetBuffer = Buffer.alloc(12); + packetBuffer[0] = 0x80; + packetBuffer[1] = 0x78; + + const { sequence, timestamp, ssrc } = connectionData; + + packetBuffer.writeUIntBE(sequence, 2, 2); + packetBuffer.writeUIntBE(timestamp, 4, 4); + packetBuffer.writeUIntBE(ssrc, 8, 4); + + packetBuffer.copy(nonce, 0, 0, 12); + return Buffer.concat([packetBuffer, ...this.encryptOpusPacket(opusPacket, connectionData)]); + } + + /** + * Encrypts an Opus packet using the format agreed upon by the instance and Discord. + * + * @param opusPacket - The Opus packet to encrypt + * @param connectionData - The current connection data of the instance + */ + private encryptOpusPacket(opusPacket: Buffer, connectionData: ConnectionData) { + const { secretKey, encryptionMode } = connectionData; + + if (encryptionMode === 'xsalsa20_poly1305_lite') { + connectionData.nonce++; + if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0; + connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0); + return [secretbox.methods.close(opusPacket, connectionData.nonceBuffer, secretKey), connectionData.nonceBuffer.slice(0, 4)]; + } else if (encryptionMode === 'xsalsa20_poly1305_suffix') { + const random = secretbox.methods.random(24, connectionData.nonceBuffer); + return [secretbox.methods.close(opusPacket, random, secretKey), random]; + } + + return [secretbox.methods.close(opusPacket, nonce, secretKey)]; + } +} diff --git a/packages/discord-voip/src/networking/VoiceUDPSocket.ts b/packages/discord-voip/src/networking/VoiceUDPSocket.ts new file mode 100644 index 0000000000..8d9ef79d52 --- /dev/null +++ b/packages/discord-voip/src/networking/VoiceUDPSocket.ts @@ -0,0 +1,180 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import { Buffer } from 'node:buffer'; +import { createSocket, type Socket } from 'node:dgram'; +import { EventEmitter } from 'node:events'; +import { isIPv4 } from 'node:net'; + +/** + * Stores an IP address and port. Used to store socket details for the local client as well as + * for Discord. + */ +export interface SocketConfig { + ip: string; + port: number; +} + +/** + * Parses the response from Discord to aid with local IP discovery. + * + * @param message - The received message + */ +export function parseLocalPacket(message: Buffer): SocketConfig { + const packet = Buffer.from(message); + + const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf8'); + + if (!isIPv4(ip)) { + throw new Error('Malformed IP address'); + } + + const port = packet.readUInt16BE(packet.length - 2); + + return { ip, port }; +} + +/** + * The interval in milliseconds at which keep alive datagrams are sent. + */ +const KEEP_ALIVE_INTERVAL = 5e3; + +/** + * The maximum value of the keep alive counter. + */ +const MAX_COUNTER_VALUE = 2 ** 32 - 1; + +export interface VoiceUDPSocket extends EventEmitter { + on(event: 'error', listener: (error: Error) => void): this; + on(event: 'close', listener: () => void): this; + on(event: 'debug', listener: (message: string) => void): this; + on(event: 'message', listener: (message: Buffer) => void): this; +} + +/** + * Manages the UDP networking for a voice connection. + */ +export class VoiceUDPSocket extends EventEmitter { + /** + * The underlying network Socket for the VoiceUDPSocket. + */ + private readonly socket: Socket; + + /** + * The socket details for Discord (remote) + */ + private readonly remote: SocketConfig; + + /** + * The counter used in the keep alive mechanism. + */ + private keepAliveCounter = 0; + + /** + * The buffer used to write the keep alive counter into. + */ + private readonly keepAliveBuffer: Buffer; + + /** + * The Node.js interval for the keep-alive mechanism. + */ + private readonly keepAliveInterval: NodeJS.Timeout; + + /** + * The time taken to receive a response to keep alive messages. + * + * @deprecated This field is no longer updated as keep alive messages are no longer tracked. + */ + public ping?: number; + + /** + * Creates a new VoiceUDPSocket. + * + * @param remote - Details of the remote socket + */ + public constructor(remote: SocketConfig) { + super(); + this.socket = createSocket('udp4'); + this.socket.on('error', (error: Error) => this.emit('error', error)); + this.socket.on('message', (buffer: Buffer) => this.onMessage(buffer)); + this.socket.on('close', () => this.emit('close')); + this.remote = remote; + this.keepAliveBuffer = Buffer.alloc(8); + this.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL); + setImmediate(() => this.keepAlive()); + } + + /** + * Called when a message is received on the UDP socket. + * + * @param buffer - The received buffer + */ + private onMessage(buffer: Buffer): void { + // Propagate the message + this.emit('message', buffer); + } + + /** + * Called at a regular interval to check whether we are still able to send datagrams to Discord. + */ + private keepAlive() { + this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0); + this.send(this.keepAliveBuffer); + this.keepAliveCounter++; + if (this.keepAliveCounter > MAX_COUNTER_VALUE) { + this.keepAliveCounter = 0; + } + } + + /** + * Sends a buffer to Discord. + * + * @param buffer - The buffer to send + */ + public send(buffer: Buffer) { + this.socket.send(buffer, this.remote.port, this.remote.ip); + } + + /** + * Closes the socket, the instance will not be able to be reused. + */ + public destroy() { + try { + this.socket.close(); + } catch { + // + } + + clearInterval(this.keepAliveInterval); + } + + /** + * Performs IP discovery to discover the local address and port to be used for the voice connection. + * + * @param ssrc - The SSRC received from Discord + */ + public async performIPDiscovery(ssrc: number): Promise { + return new Promise((resolve, reject) => { + const listener = (message: Buffer) => { + try { + if (message.readUInt16BE(0) !== 2) return; + const packet = parseLocalPacket(message); + this.socket.off('message', listener); + resolve(packet); + } catch { + // + } + }; + + this.socket.on('message', listener); + this.socket.once('close', () => reject(new Error('Cannot perform IP discovery - socket closed'))); + + const discoveryBuffer = Buffer.alloc(74); + + discoveryBuffer.writeUInt16BE(1, 0); + discoveryBuffer.writeUInt16BE(70, 2); + discoveryBuffer.writeUInt32BE(ssrc, 4); + this.send(discoveryBuffer); + }); + } +} diff --git a/packages/discord-voip/src/networking/VoiceWebSocket.ts b/packages/discord-voip/src/networking/VoiceWebSocket.ts new file mode 100644 index 0000000000..01a63b2f4a --- /dev/null +++ b/packages/discord-voip/src/networking/VoiceWebSocket.ts @@ -0,0 +1,180 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import { EventEmitter } from 'node:events'; +import { VoiceOpcodes } from 'discord-api-types/voice/v4'; +import WebSocket, { type MessageEvent } from 'ws'; +import { unsafe } from '../common/types'; + +export interface VoiceWebSocket extends EventEmitter { + on(event: 'error', listener: (error: Error) => void): this; + on(event: 'open', listener: (event: WebSocket.Event) => void): this; + on(event: 'close', listener: (event: WebSocket.CloseEvent) => void): this; + /** + * Debug event for VoiceWebSocket. + * + * @eventProperty + */ + on(event: 'debug', listener: (message: string) => void): this; + /** + * Packet event. + * + * @eventProperty + */ + on(event: 'packet', listener: (packet: unsafe) => void): this; +} + +/** + * An extension of the WebSocket class to provide helper functionality when interacting + * with the Discord Voice gateway. + */ +export class VoiceWebSocket extends EventEmitter { + /** + * The current heartbeat interval, if any. + */ + private heartbeatInterval?: NodeJS.Timeout; + + /** + * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received. + * This is set to 0 if an acknowledgement packet hasn't been received yet. + */ + private lastHeartbeatAck: number; + + /** + * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat + * hasn't been sent yet. + */ + private lastHeartbeatSend: number; + + /** + * The number of consecutively missed heartbeats. + */ + private missedHeartbeats = 0; + + /** + * The last recorded ping. + */ + public ping?: number; + + /** + * The debug logger function, if debugging is enabled. + */ + private readonly debug: ((message: string) => void) | null; + + /** + * The underlying WebSocket of this wrapper. + */ + private readonly ws: WebSocket; + + /** + * Creates a new VoiceWebSocket. + * + * @param address - The address to connect to + */ + public constructor(address: string, debug: boolean) { + super(); + this.ws = new WebSocket(address); + this.ws.onmessage = (err) => this.onMessage(err); + this.ws.onopen = (err) => this.emit('open', err); + this.ws.onerror = (err: Error | WebSocket.ErrorEvent) => this.emit('error', err instanceof Error ? err : err.error); + this.ws.onclose = (err) => this.emit('close', err); + + this.lastHeartbeatAck = 0; + this.lastHeartbeatSend = 0; + + this.debug = debug ? (message: string) => this.emit('debug', message) : null; + } + + /** + * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed. + */ + public destroy() { + try { + this.debug?.('destroyed'); + this.setHeartbeatInterval(-1); + this.ws.close(1_000); + } catch (error) { + const err = error as Error; + this.emit('error', err); + } + } + + /** + * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them + * as packets. + * + * @param event - The message event + */ + public onMessage(event: MessageEvent) { + if (typeof event.data !== 'string') return; + + this.debug?.(`<< ${event.data}`); + + let packet: unsafe; + try { + packet = JSON.parse(event.data); + } catch (error) { + const err = error as Error; + this.emit('error', err); + return; + } + + if (packet.op === VoiceOpcodes.HeartbeatAck) { + this.lastHeartbeatAck = Date.now(); + this.missedHeartbeats = 0; + this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend; + } + + this.emit('packet', packet); + } + + /** + * Sends a JSON-stringifiable packet over the WebSocket. + * + * @param packet - The packet to send + */ + public sendPacket(packet: unsafe) { + try { + const stringified = JSON.stringify(packet); + this.debug?.(`>> ${stringified}`); + this.ws.send(stringified); + } catch (error) { + const err = error as Error; + this.emit('error', err); + } + } + + /** + * Sends a heartbeat over the WebSocket. + */ + private sendHeartbeat() { + this.lastHeartbeatSend = Date.now(); + this.missedHeartbeats++; + const nonce = this.lastHeartbeatSend; + this.sendPacket({ + op: VoiceOpcodes.Heartbeat, + // eslint-disable-next-line id-length + d: nonce + }); + } + + /** + * Sets/clears an interval to send heartbeats over the WebSocket. + * + * @param ms - The interval in milliseconds. If negative, the interval will be unset + */ + public setHeartbeatInterval(ms: number) { + if (this.heartbeatInterval !== undefined) clearInterval(this.heartbeatInterval); + if (ms > 0) { + this.heartbeatInterval = setInterval(() => { + if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) { + // Missed too many heartbeats - disconnect + this.ws.close(); + this.setHeartbeatInterval(-1); + } + + this.sendHeartbeat(); + }, ms); + } + } +} diff --git a/packages/discord-voip/src/networking/index.ts b/packages/discord-voip/src/networking/index.ts new file mode 100644 index 0000000000..bce5ebbf04 --- /dev/null +++ b/packages/discord-voip/src/networking/index.ts @@ -0,0 +1,6 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +export * from './Networking'; +export * from './VoiceUDPSocket'; +export * from './VoiceWebSocket'; diff --git a/packages/discord-voip/src/util/Secretbox.ts b/packages/discord-voip/src/util/Secretbox.ts new file mode 100644 index 0000000000..8bf09fbad3 --- /dev/null +++ b/packages/discord-voip/src/util/Secretbox.ts @@ -0,0 +1,85 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import { Buffer } from 'node:buffer'; +import { unsafe } from '../common/types'; + +interface Methods { + close(opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer; + open(buffer: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer | null; + random(bytes: number, nonce: Buffer): Buffer; +} + +const libs = { + 'sodium-native': (sodium: unsafe): Methods => ({ + open: (buffer: Buffer, nonce: Buffer, secretKey: Uint8Array) => { + if (buffer) { + const output = Buffer.allocUnsafe(buffer.length - sodium.crypto_box_MACBYTES); + if (sodium.crypto_secretbox_open_easy(output, buffer, nonce, secretKey)) return output; + } + + return null; + }, + close: (opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array) => { + const output = Buffer.allocUnsafe(opusPacket.length + sodium.crypto_box_MACBYTES); + sodium.crypto_secretbox_easy(output, opusPacket, nonce, secretKey); + return output; + }, + random: (num: number, buffer: Buffer = Buffer.allocUnsafe(num)) => { + sodium.randombytes_buf(buffer); + return buffer; + } + }), + sodium: (sodium: unsafe): Methods => ({ + open: sodium.api.crypto_secretbox_open_easy, + close: sodium.api.crypto_secretbox_easy, + random: (num: number, buffer: Buffer = Buffer.allocUnsafe(num)) => { + sodium.api.randombytes_buf(buffer); + return buffer; + } + }), + 'libsodium-wrappers': (sodium: unsafe): Methods => ({ + open: sodium.crypto_secretbox_open_easy, + close: sodium.crypto_secretbox_easy, + random: sodium.randombytes_buf + }), + tweetnacl: (tweetnacl: unsafe): Methods => ({ + open: tweetnacl.secretbox.open, + close: tweetnacl.secretbox, + random: tweetnacl.randomBytes + }) +}; + +// @ts-ignore +libs['sodium-javascript'] = libs['sodium-native']; + +const fallbackError = () => { + const libsName = Object.keys(libs).join(', '); + throw new Error( + `Cannot play audio as no valid encryption package is installed. +- Install one of the following packages: ${libsName} +- Use the console.log(DependencyReport.generateString()) function for more information.\n` + ); +}; + +const methods: Methods = { + open: fallbackError, + close: fallbackError, + random: fallbackError +}; + +void (async () => { + for (const libName of Object.keys(libs) as (keyof typeof libs)[]) { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + const lib = require(libName); + if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; + Object.assign(methods, libs[libName](lib)); + break; + } catch { + // + } + } +})(); + +export { methods }; diff --git a/packages/discord-voip/src/util/abortAfter.ts b/packages/discord-voip/src/util/abortAfter.ts new file mode 100644 index 0000000000..1e7c570e90 --- /dev/null +++ b/packages/discord-voip/src/util/abortAfter.ts @@ -0,0 +1,14 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +/** + * Creates an abort controller that aborts after the given time. + * + * @param delay - The time in milliseconds to wait before aborting + */ +export function abortAfter(delay: number): [AbortController, AbortSignal] { + const ac = new AbortController(); + const timeout = setTimeout(() => ac.abort(), delay); + ac.signal.addEventListener('abort', () => clearTimeout(timeout)); + return [ac, ac.signal]; +} diff --git a/packages/discord-voip/src/util/adapter.ts b/packages/discord-voip/src/util/adapter.ts new file mode 100644 index 0000000000..658da7ad48 --- /dev/null +++ b/packages/discord-voip/src/util/adapter.ts @@ -0,0 +1,54 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10'; +import { unsafe } from '../common/types'; + +/** + * Methods that are provided by the \@discordjs/voice library to implementations of + * Discord gateway DiscordGatewayAdapters. + */ +export interface DiscordGatewayAdapterLibraryMethods { + /** + * Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway) + */ + destroy(): void; + /** + * Call this when you receive a VOICE_SERVER_UPDATE payload that is relevant to the adapter. + * + * @param data - The inner data of the VOICE_SERVER_UPDATE payload + */ + onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void; + /** + * Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter. + * + * @param data - The inner data of the VOICE_STATE_UPDATE payload + */ + onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void; +} + +/** + * Methods that are provided by the implementer of a Discord gateway DiscordGatewayAdapter. + */ +export interface DiscordGatewayAdapterImplementerMethods { + /** + * This will be called by \@discordjs/voice when the adapter can safely be destroyed as it will no + * longer be used. + */ + destroy(): void; + /** + * Implement this method such that the given payload is sent to the main Discord gateway connection. + * + * @param payload - The payload to send to the main Discord gateway connection + * @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects + */ + sendPayload(payload: unsafe): boolean; +} + +/** + * A function used to build adapters. It accepts a methods parameter that contains functions that + * can be called by the implementer when new data is received on its gateway connection. In return, + * the implementer will return some methods that the library can call - e.g. to send messages on + * the gateway, or to signal that the adapter can be removed. + */ +export type DiscordGatewayAdapterCreator = (methods: DiscordGatewayAdapterLibraryMethods) => DiscordGatewayAdapterImplementerMethods; diff --git a/packages/discord-voip/src/util/demuxProbe.ts b/packages/discord-voip/src/util/demuxProbe.ts new file mode 100644 index 0000000000..34607df686 --- /dev/null +++ b/packages/discord-voip/src/util/demuxProbe.ts @@ -0,0 +1,128 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import { Buffer } from 'node:buffer'; +import process from 'node:process'; +import { Readable } from 'node:stream'; +import prism from 'prism-media'; +import { StreamType } from '..'; +import { noop } from './util'; + +/** + * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel. + * + * @param opusHead - The Opus Head to validate + * @returns `true` if suitable to play in a Discord voice channel, otherwise `false` + */ +export function validateDiscordOpusHead(opusHead: Buffer): boolean { + const channels = opusHead.readUInt8(9); + const sampleRate = opusHead.readUInt32LE(12); + return channels === 2 && sampleRate === 48_000; +} + +/** + * The resulting information after probing an audio stream + */ +export interface ProbeInfo { + /** + * The readable audio stream to use. You should use this rather than the input stream, as the probing + * function can sometimes read the input stream to its end and cause the stream to close. + */ + stream: Readable; + + /** + * The recommended stream type for this audio stream. + */ + type: StreamType; +} + +/** + * Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer. + * + * @param stream - The readable stream to probe + * @param probeSize - The number of bytes to attempt to read before giving up on the probe + * @param validator - The Opus Head validator function + * @experimental + */ +export async function demuxProbe(stream: Readable, probeSize = 1_024, validator = validateDiscordOpusHead): Promise { + return new Promise((resolve, reject) => { + // Preconditions + if (stream.readableObjectMode) { + reject(new Error('Cannot probe a readable stream in object mode')); + return; + } + + if (stream.readableEnded) { + reject(new Error('Cannot probe a stream that has ended')); + return; + } + + let readBuffer = Buffer.alloc(0); + + let resolved: StreamType | undefined; + + const finish = (type: StreamType) => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + stream.off('data', onData); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + stream.off('close', onClose); + // eslint-disable-next-line @typescript-eslint/no-use-before-define + stream.off('end', onClose); + stream.pause(); + resolved = type; + if (stream.readableEnded) { + resolve({ + stream: Readable.from(readBuffer), + type + }); + } else { + if (readBuffer.length > 0) { + stream.push(readBuffer); + } + + resolve({ + stream, + type + }); + } + }; + + const foundHead = (type: StreamType) => (head: Buffer) => { + if (validator(head)) { + finish(type); + } + }; + + const webm = new prism.opus.WebmDemuxer(); + webm.once('error', noop); + webm.on('head', foundHead(StreamType.WebmOpus)); + + const ogg = new prism.opus.OggDemuxer(); + ogg.once('error', noop); + ogg.on('head', foundHead(StreamType.OggOpus)); + + const onClose = () => { + if (!resolved) { + finish(StreamType.Arbitrary); + } + }; + + const onData = (buffer: Buffer) => { + readBuffer = Buffer.concat([readBuffer, buffer]); + + webm.write(buffer); + ogg.write(buffer); + + if (readBuffer.length >= probeSize) { + stream.off('data', onData); + stream.pause(); + process.nextTick(onClose); + } + }; + + stream.once('error', reject); + stream.on('data', onData); + stream.once('close', onClose); + stream.once('end', onClose); + }); +} diff --git a/packages/discord-voip/src/util/entersState.ts b/packages/discord-voip/src/util/entersState.ts new file mode 100644 index 0000000000..58ae4f5e70 --- /dev/null +++ b/packages/discord-voip/src/util/entersState.ts @@ -0,0 +1,45 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +import { type EventEmitter, once } from 'node:events'; +import type { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection'; +import type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer'; +import { abortAfter } from './abortAfter'; + +/** + * Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error. + * + * @param target - The voice connection that we want to observe the state change for + * @param status - The status that the voice connection should be in + * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation + */ +export function entersState(target: VoiceConnection, status: VoiceConnectionStatus, timeoutOrSignal: AbortSignal | number): Promise; + +/** + * Allows an audio player a specified amount of time to enter a given state, otherwise rejects with an error. + * + * @param target - The audio player that we want to observe the state change for + * @param status - The status that the audio player should be in + * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation + */ +export function entersState(target: AudioPlayer, status: AudioPlayerStatus, timeoutOrSignal: AbortSignal | number): Promise; + +/** + * Allows a target a specified amount of time to enter a given state, otherwise rejects with an error. + * + * @param target - The object that we want to observe the state change for + * @param status - The status that the target should be in + * @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation + */ +export async function entersState(target: Target, status: AudioPlayerStatus | VoiceConnectionStatus, timeoutOrSignal: AbortSignal | number) { + if (target.state.status !== status) { + const [ac, signal] = typeof timeoutOrSignal === 'number' ? abortAfter(timeoutOrSignal) : [undefined, timeoutOrSignal]; + try { + await once(target as EventEmitter, status, { signal }); + } finally { + ac?.abort(); + } + } + + return target; +} diff --git a/packages/discord-voip/src/util/index.ts b/packages/discord-voip/src/util/index.ts new file mode 100644 index 0000000000..e4ae755459 --- /dev/null +++ b/packages/discord-voip/src/util/index.ts @@ -0,0 +1,6 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +export * from './entersState'; +export * from './adapter'; +export * from './demuxProbe'; diff --git a/packages/discord-voip/src/util/util.ts b/packages/discord-voip/src/util/util.ts new file mode 100644 index 0000000000..003b05928f --- /dev/null +++ b/packages/discord-voip/src/util/util.ts @@ -0,0 +1,4 @@ +// Copyright discord-player authors. All rights reserved. MIT License. +// Copyright discord.js authors. All rights reserved. Apache License 2.0 + +export const noop = () => {}; diff --git a/packages/ffmpeg/src/FFmpeg.ts b/packages/ffmpeg/src/FFmpeg.ts index 1cb4dc22d7..15bd0fd590 100644 --- a/packages/ffmpeg/src/FFmpeg.ts +++ b/packages/ffmpeg/src/FFmpeg.ts @@ -1,18 +1,19 @@ -import childProcess from 'child_process'; -import { Duplex, DuplexOptions } from 'stream'; +import { ChildProcessWithoutNullStreams, spawn, spawnSync } from 'node:child_process'; +import { Duplex, DuplexOptions } from 'node:stream'; -type Callback> = (...args: Args) => unknown; +export type FFmpegLib = 'ffmpeg' | './ffmpeg' | 'avconv' | './avconv' | 'ffmpeg-static' | '@ffmpeg-installer/ffmpeg' | '@node-ffmpeg/node-ffmpeg-installer' | 'ffmpeg-binaries'; -const validatePathParam = (t: unknown, name?: string) => { - if (typeof t !== 'string' || !t) throw new TypeError(`Expected ${name ? name.concat(' to be ') : ''}a string, got ${t}`); - return t; -}; +export type FFmpegCallback> = (...args: Args) => unknown; + +export interface FFmpegSource { + name: FFmpegLib; + module: boolean; +} -export interface FFmpegInfo { - command: string | null; - metadata: string | null; - version: string | null; - isStatic: boolean; +export interface ResolvedFFmpegSource extends FFmpegSource { + path: string; + version: string; + command: string; } export interface FFmpegOptions extends DuplexOptions { @@ -20,168 +21,134 @@ export interface FFmpegOptions extends DuplexOptions { shell?: boolean; } -const ffmpegInfo: FFmpegInfo = { - command: null, - metadata: null, - version: null, - isStatic: false -}; - -interface FFmpegLocation { - displayName: string; - getPath: () => string; -} +const VERSION_REGEX = /version (.+) Copyright/im; -const isWindows = process.platform === 'win32'; - -/* eslint-disable @typescript-eslint/no-var-requires */ -// prettier-ignore -export const FFmpegPossibleLocations: FFmpegLocation[] = [ - { - getPath() { - return validatePathParam(process.env.FFMPEG_PATH, this.displayName); - }, - displayName: 'spawn process.env.FFMPEG_PATH' - }, - { - getPath() { - return 'ffmpeg'; - }, - displayName: 'spawn ffmpeg' - }, - { - getPath() { - return 'avconv'; - }, - displayName: 'spawn avconv' - }, - { - getPath() { - const loc = './ffmpeg'; - if (isWindows) return loc.concat('.exe'); - return loc; - }, - displayName: 'spawn ./ffmpeg' - }, - { - getPath() { - const loc = './avconv'; - if (isWindows) return loc.concat('.exe'); - return loc; - }, - displayName: 'spawn ./avconv' - }, - { - getPath() { - const mod = require('@ffmpeg-installer/ffmpeg'); - return validatePathParam(mod.default?.path || mod.path || mod, this.displayName); - }, - displayName: 'require("@ffmpeg-installer/ffmpeg")' - }, - { - getPath() { - const mod = require('ffmpeg-static'); - return validatePathParam(mod.default?.path || mod.path || mod, this.displayName); - }, - displayName: 'require("ffmpeg-static")' - }, - { - getPath() { - const mod = require('@node-ffmpeg/node-ffmpeg-installer'); - return validatePathParam(mod.default?.path || mod.path || mod, this.displayName); - }, - displayName: 'require("@node-ffmpeg/node-ffmpeg-installer")' - }, - { - getPath() { - const mod = require('ffmpeg-binaries'); - return validatePathParam(mod.default || mod, this.displayName); - }, - displayName: 'require("ffmpeg-binaries")' - } -]; -/* eslint-enable @typescript-eslint/no-var-requires */ +const validatePathParam = (path: string, displayName: string) => { + if (!path) throw new Error(`Failed to resolve ${displayName}`); + return path; +}; export class FFmpeg extends Duplex { /** - * FFmpeg version regex + * Cached FFmpeg source. */ - public static VersionRegex = /version (.+) Copyright/im; - + private static cached: ResolvedFFmpegSource | null = null; /** - * Spawns ffmpeg process - * @param options Spawn options + * Supported FFmpeg sources. */ - public static spawn({ args = [] as string[], shell = false } = {}) { - if (!args.includes('-i')) args.unshift('-i', '-'); + public static sources: FFmpegSource[] = [ + // paths + { name: 'ffmpeg', module: false }, + { name: './ffmpeg', module: false }, + { name: 'avconv', module: false }, + { name: './avconv', module: false }, + // modules + { name: 'ffmpeg-static', module: true }, + { name: '@ffmpeg-installer/ffmpeg', module: true }, + { name: '@node-ffmpeg/node-ffmpeg-installer', module: true }, + { name: 'ffmpeg-binaries', module: true } + ]; - return childProcess.spawn(this.locate()!.command!, args.concat(['pipe:1']), { windowsHide: true, shell }); + /** + * Checks if FFmpeg is loaded. + */ + public static isLoaded() { + return FFmpeg.cached != null; } /** - * Check if ffmpeg is available + * Adds a new FFmpeg source. + * @param source FFmpeg source */ - public static isAvailable() { - return typeof this.locateSafe(false)?.command === 'string'; + public static addSource(source: FFmpegSource) { + if (FFmpeg.sources.some((s) => s.name === source.name)) return false; + FFmpeg.sources.push(source); + return true; } /** - * Safe locate ffmpeg - * @param force if it should relocate the command + * Removes a FFmpeg source. + * @param source FFmpeg source */ - public static locateSafe(force = false) { - try { - return this.locate(force); - } catch { - return null; - } + public static removeSource(source: FFmpegSource) { + const index = FFmpeg.sources.findIndex((s) => s.name === source.name); + if (index === -1) return false; + FFmpeg.sources.splice(index, 1); + return true; } /** - * Locate ffmpeg command. Throws error if ffmpeg is not found. - * @param force Forcefully reload + * Resolves FFmpeg path. Throws an error if it fails to resolve. + * @param force if it should relocate the command */ - public static locate(force = false): FFmpegInfo | undefined { - if (ffmpegInfo.command && !force) return ffmpegInfo; + public static resolve(force = false) { + if (!force && FFmpeg.cached) return FFmpeg.cached; - const errStacks: Error[] = new Array(FFmpegPossibleLocations.length); + const errors: string[] = []; - for (const locator of FFmpegPossibleLocations) { - if (locator == null) continue; + for (const source of FFmpeg.sources) { try { - const command = locator.getPath(); + let path: string; + + if (source.module) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const mod = require(source.name); + path = validatePathParam(mod.default?.path || mod.path || mod, source.name); + } else { + path = source.name; + } + + const result = spawnSync(path, ['-v'], { windowsHide: true }); - const result = childProcess.spawnSync(command, ['-h'], { - windowsHide: true - }); + const resolved: ResolvedFFmpegSource = { + command: path, + module: source.module, + name: source.name, + path, + version: VERSION_REGEX.exec(result.stderr.toString())?.[1] ?? 'unknown' + }; - if (result.error) throw result.error; + FFmpeg.cached = resolved; - ffmpegInfo.command = command; - ffmpegInfo.metadata = Buffer.concat(result.output.filter(Boolean) as Buffer[]).toString(); - ffmpegInfo.isStatic = locator.displayName.startsWith('require("'); - ffmpegInfo.version = FFmpeg.VersionRegex.exec(ffmpegInfo.metadata || '')?.[1] || null; + errors.length = 0; - return ffmpegInfo; + return resolved; } catch (e) { - errStacks.push(e as Error); + const err = e && e instanceof Error ? e.message : `${e}`; + const msg = `Failed to load ffmpeg using ${source.module ? `require('${source.name}')` : `spawn('${source.name}')`}. Error: ${err}`; + + errors.push(msg); } } - // prettier-ignore - throw new Error([ - 'Could not locate ffmpeg. Tried:\n', - ...FFmpegPossibleLocations.map((loc, i) => ` ${++i}. ${loc.displayName}`), - '\n', - `${'='.repeat(5)}Full Stacktrace${'='.repeat(5)}`, - ...errStacks.map((e) => e.stack || e.message) - ].join('\n')); + throw new Error(`Could not load ffmpeg. Errors:\n${errors.join('\n')}`); + } + + /** + * Resolves FFmpeg path safely. Returns null if it fails to resolve. + * @param force if it should relocate the command + */ + public static resolveSafe(force = false) { + try { + return FFmpeg.resolve(force); + } catch { + return null; + } + } + + /** + * Spawns ffmpeg process + * @param options Spawn options + */ + public static spawn({ args = [] as string[], shell = false } = {}) { + if (!args.includes('-i')) args.unshift('-i', '-'); + return spawn(FFmpeg.resolve().command, args.concat(['pipe:1']), { windowsHide: true, shell }); } /** * Current FFmpeg process */ - public process: childProcess.ChildProcessWithoutNullStreams; + public process: ChildProcessWithoutNullStreams; /** * Create FFmpeg duplex stream @@ -250,12 +217,12 @@ export class FFmpeg extends Duplex { } } - public _destroy(err: Error | null, cb: Callback<[Error | null]>) { + public _destroy(err: Error | null, cb: FFmpegCallback<[Error | null]>) { this._cleanup(); if (cb) return cb(err); } - public _final(cb: Callback<[]>) { + public _final(cb: FFmpegCallback<[]>) { this._cleanup(); cb(); } @@ -266,15 +233,7 @@ export class FFmpeg extends Duplex { // }); this.process.kill('SIGKILL'); - this.process = null as unknown as childProcess.ChildProcessWithoutNullStreams; + this.process = null as unknown as ChildProcessWithoutNullStreams; } } - - public toString() { - if (!ffmpegInfo.metadata) return 'FFmpeg'; - - return ffmpegInfo.metadata; - } } - -export const findFFmpeg = FFmpeg.locate; diff --git a/packages/ffmpeg/src/index.ts b/packages/ffmpeg/src/index.ts index 5db16ec9b9..7b6cb01bd7 100644 --- a/packages/ffmpeg/src/index.ts +++ b/packages/ffmpeg/src/index.ts @@ -1,4 +1,27 @@ export * from './FFmpeg'; +export type ArgPrimitive = string | number; + +/** + * Create FFmpeg arguments from an object. + * @param input The input object. + * @returns The FFmpeg arguments. + * @example createFFmpegArgs({ i: 'input.mp3', af: ['bass=g=10','acompressor'] }, './out.mp3'); + * // => ['-i', 'input.mp3', '-af', 'bass=g=10,acompressor', './out.mp3'] + */ +export const createFFmpegArgs = (input: Record, post?: string | string[]): string[] => { + const args = []; + + for (const [key, value] of Object.entries(input)) { + args.push(`-${key}`, String(value)); + } + + if (post) { + Array.isArray(post) ? args.push(...post) : args.push(post); + } + + return args; +}; + // eslint-disable-next-line @typescript-eslint/no-inferrable-types export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/node/LICENSE b/packages/node/LICENSE new file mode 100644 index 0000000000..fe07fc7364 --- /dev/null +++ b/packages/node/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Androz2091 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/node/README.md b/packages/node/README.md new file mode 100644 index 0000000000..0c225f82ea --- /dev/null +++ b/packages/node/README.md @@ -0,0 +1,15 @@ +# `@discord-player/node` + +Discord Player `@discord-player/node` library + +## Installation + +```sh +$ yarn add @discord-player/node +``` + +## Example + +```js +import pkg from "@discord-player/node" +``` \ No newline at end of file diff --git a/packages/node/__test__/sum.spec.ts b/packages/node/__test__/sum.spec.ts new file mode 100644 index 0000000000..6d3cd1af62 --- /dev/null +++ b/packages/node/__test__/sum.spec.ts @@ -0,0 +1,8 @@ +import { add } from '../src'; +import { describe, it, expect } from 'vitest'; + +describe('Sum', () => { + it('should add two numbers', () => { + expect(add(2, 2)).toBe(4); + }); +}); diff --git a/packages/node/package.json b/packages/node/package.json new file mode 100644 index 0000000000..ea599512f6 --- /dev/null +++ b/packages/node/package.json @@ -0,0 +1,42 @@ +{ + "name": "@discord-player/node", + "version": "0.1.0", + "description": "A complete framework to simplify the implementation of music commands for Discord bots", + "keywords": [ + "discord-player", + "music", + "bot", + "discord.js", + "javascript", + "voip", + "lavalink", + "lavaplayer" + ], + "author": "Androz2091 ", + "homepage": "https://discord-player.js.org", + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Androz2091/discord-player.git" + }, + "scripts": { + "build": "tsup", + "build:check": "tsc --noEmit", + "lint": "eslint src --ext .ts --fix", + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "bugs": { + "url": "https://github.com/Androz2091/discord-player/issues" + }, + "devDependencies": { + "@discord-player/tsconfig": "workspace:^", + "tsup": "^8.1.0" + } +} diff --git a/packages/node/src/PlayerNode.ts b/packages/node/src/PlayerNode.ts new file mode 100644 index 0000000000..06421d3386 --- /dev/null +++ b/packages/node/src/PlayerNode.ts @@ -0,0 +1,178 @@ +import { randomUUID } from 'node:crypto'; + +/** + * Represents a player node object. + */ +export interface PlayerNodeOptions { + /** + * The host of the node. + */ + host: string; + /** + * The port of the node, if any. + */ + port: number | null; + /** + * Whether the node uses a secure connection. + */ + secure: boolean; + /** + * The client ID of this node. + */ + clientId: string; + /** + * The password of this node. + */ + password: string; +} + +/** + * Represents a player node string. + * + * Non secure - `discord-player://clientId:password@localhost:2333` + * + * Secure - `discord-player://clientId:password@localhost:2333?secure=true` + */ +export type PlayerNodeString = `discord-player://${string}`; + +/** + * Represents a player node type. + */ +export type PlayerNodeLike = PlayerNodeOptions | PlayerNodeString | URL; + +export interface PlayerNodeInit { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + send?: (packet: Record) => Awaited; +} + +/** + * The protocol for discord player nodes. + */ +export const DISCORD_PLAYER_PROTOCOL = 'discord-player:'; + +function validateNodeOptions(node: PlayerNodeOptions): PlayerNodeOptions { + const required = ['host', 'clientId', 'password'] as const; + + for (const key of required) { + if (typeof node[key] !== 'string') { + throw new Error(`Expected key 'NodeOptions.${key}' to be a string.`); + } + } + + if ('port' in node && node.port !== null && typeof node.port !== 'number') { + throw new Error("Expected key 'NodeOptions.port' to be a number."); + } + + if ('secure' in node && typeof node.secure !== 'boolean') { + throw new Error("Expected key 'NodeOptions.secure' to be a boolean."); + } + + // defaults + node.secure ??= false; + + return node; +} + +export class PlayerNode { + /** + * The unique identifier for this player node. + */ + public readonly id = randomUUID(); + + /** + * The resolved player node config. + */ + private readonly node: PlayerNodeOptions; + + /** + * Creates a new player node. + * @param node - The node config or node string to use + * @param options - The player node options + */ + public constructor(node: PlayerNodeLike, private readonly options: PlayerNodeInit) { + if (typeof node === 'string' || node instanceof URL) { + this.node = PlayerNode.parseNode(node); + } else { + this.node = validateNodeOptions(node); + } + } + + /** + * Connects to this player node. + */ + public async connect() {} + + /** + * Destroy this player node. + */ + public async delete() {} + + /** + * Sends a packet to the player node. + */ + public isSecure(): boolean { + return this.node.secure; + } + + /** + * Sends a packet to the player node. + * @param censorPassword - Whether to censor the password in the URL + */ + public getURL(censorPassword = false): URL { + const { host, port, secure, clientId, password } = this.node; + + const pwd = censorPassword ? password.replace(/./g, '*') : password; + const url = new URL(`${DISCORD_PLAYER_PROTOCOL}//${clientId}:${pwd}@${host}:${port}`); + + if (secure) { + url.searchParams.set('secure', 'true'); + } + + return url; + } + + /** + * JSON representation of the player node. + */ + public toJSON() { + return this.node; + } + + /** + * String representation of the player node. + */ + public toString() { + return this.getURL(true).toString(); + } + + /** + * Parses a player node string into a player node options object. + * @param node - The node string to parse + * @returns The parsed player node options + * @example const node = PlayerNode.parseNodeString('discord-player://clientId:password@localhost:2333?secure=true'); + * // => { host: 'localhost', port: 2333, secure: true, clientId: 'clientId', password: 'password' } + */ + public static parseNode(node: PlayerNodeString | URL): PlayerNodeOptions { + if (!(node instanceof URL) && !URL.canParse(node)) { + throw new Error('Invalid node string.'); + } + + const url = node instanceof URL ? node : new URL(node); + + const { username, password, hostname, searchParams, port, protocol } = url; + + if (protocol !== DISCORD_PLAYER_PROTOCOL) { + throw new Error('Unsupported protocol in node string.'); + } + + const isSecure = searchParams.get('secure') === 'true'; + + return { + host: hostname, + port: port ? Number(port) : null, + secure: isSecure, + clientId: username, + password + }; + } +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts new file mode 100644 index 0000000000..459a045730 --- /dev/null +++ b/packages/node/src/index.ts @@ -0,0 +1,4 @@ +export * from './PlayerNode'; + +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json new file mode 100644 index 0000000000..3c774c914c --- /dev/null +++ b/packages/node/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@discord-player/tsconfig/base.json", + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/packages/node/tsup.config.ts b/packages/node/tsup.config.ts new file mode 100644 index 0000000000..e161ba4d4d --- /dev/null +++ b/packages/node/tsup.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from '../../tsup.config'; +import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; + +export default defineConfig({ + esbuildPlugins: [esbuildPluginVersionInjector()] +}); diff --git a/packages/node/vitest.config.ts b/packages/node/vitest.config.ts new file mode 100644 index 0000000000..e055dbaa74 --- /dev/null +++ b/packages/node/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + dir: `${__dirname}/__test__`, + passWithNoTests: true, + watch: false + } +}); diff --git a/packages/opus/src/OggDemuxer.ts b/packages/opus/src/OggDemuxer.ts new file mode 100644 index 0000000000..ca67f9616a --- /dev/null +++ b/packages/opus/src/OggDemuxer.ts @@ -0,0 +1,134 @@ +// based on https://github.com/amishshah/prism-media/blob/4ef1d6f9f53042c085c1f68627e889003e248d77/src/opus/OggDemuxer.js + +import { Transform, TransformCallback } from 'node:stream'; + +const OGG_PAGE_HEADER_SIZE = 26; +const STREAM_STRUCTURE_VERSION = 0; + +const charCode = (x: string) => x.charCodeAt(0); +const OGGS_HEADER = Buffer.from([...'OggS'].map(charCode)); +const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode)); +const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode)); + +/** + * Demuxes an Ogg stream (containing Opus audio) to output an Opus stream. + */ +export class OggDemuxer extends Transform { + private _remainder: Buffer | null = null; + private _head: Buffer | null = null; + private _bitstream: number | null = null; + + /** + * Creates a new OggOpus demuxer. + * @param {Object} [options] options that you would pass to a regular Transform stream. + * @memberof opus + */ + public constructor(options = {}) { + super(Object.assign({ readableObjectMode: true }, options)); + this._remainder = null; + this._head = null; + this._bitstream = null; + } + + _transform(chunk: Buffer, encoding: BufferEncoding, done: TransformCallback) { + if (this._remainder) { + chunk = Buffer.concat([this._remainder, chunk]); + this._remainder = null; + } + + try { + while (chunk) { + const result = this._readPage(chunk); + if (result) chunk = result; + else break; + } + } catch (error) { + done(error as Error); + return; + } + + this._remainder = chunk; + done(); + } + + /** + * Reads a page from a buffer + * @private + * @param {Buffer} chunk the chunk containing the page + * @returns {boolean|Buffer} if a buffer, it will be a slice of the excess data of the original, otherwise it will be + * false and would indicate that there is not enough data to go ahead with reading this page. + */ + _readPage(chunk: Buffer) { + if (chunk.length < OGG_PAGE_HEADER_SIZE) { + return false; + } + if (!chunk.subarray(0, 4).equals(OGGS_HEADER)) { + throw Error(`capture_pattern is not ${OGGS_HEADER}`); + } + if (chunk.readUInt8(4) !== STREAM_STRUCTURE_VERSION) { + throw Error(`stream_structure_version is not ${STREAM_STRUCTURE_VERSION}`); + } + + if (chunk.length < 27) return false; + const pageSegments = chunk.readUInt8(26); + if (chunk.length < 27 + pageSegments) return false; + const table = chunk.subarray(27, 27 + pageSegments); + const bitstream = chunk.readUInt32BE(14); + + const sizes: number[] = []; + let totalSize = 0; + + for (let i = 0; i < pageSegments; ) { + let size = 0, + x = 255; + while (x === 255) { + if (i >= table.length) return false; + x = table.readUInt8(i); + i++; + size += x; + } + sizes.push(size); + totalSize += size; + } + + if (chunk.length < 27 + pageSegments + totalSize) return false; + + let start = 27 + pageSegments; + for (const size of sizes) { + const segment = chunk.subarray(start, start + size); + const header = segment.subarray(0, 8); + if (this._head) { + if (header.equals(OPUS_TAGS)) this.emit('tags', segment); + else if (this._bitstream === bitstream) this.push(segment); + } else if (header.equals(OPUS_HEAD)) { + this.emit('head', segment); + this._head = segment; + this._bitstream = bitstream; + } else { + this.emit('unknownSegment', segment); + } + start += size; + } + return chunk.subarray(start); + } + + _destroy(err: Error, cb: (error: Error | null) => void) { + this._cleanup(); + return cb ? cb(err) : undefined; + } + + _final(cb: TransformCallback) { + this._cleanup(); + cb(); + } + + /** + * Cleans up the demuxer when it is no longer required. + * @private + */ + _cleanup() { + this._remainder = null; + this._head = null; + this._bitstream = null; + } +} diff --git a/packages/opus/src/OpusEncoder.ts b/packages/opus/src/OpusEncoder.ts new file mode 100644 index 0000000000..37d6c35649 --- /dev/null +++ b/packages/opus/src/OpusEncoder.ts @@ -0,0 +1,379 @@ +// based on https://github.com/amishshah/prism-media/blob/4ef1d6f9f53042c085c1f68627e889003e248d77/src/opus/Opus.js + +import { Transform, type TransformCallback } from 'node:stream'; + +export type IEncoder = { + new (rate: number, channels: number, application: number): { + encode(buffer: Buffer): Buffer; + encode(buffer: Buffer, frameSize: number): Buffer; + encode(buffer: Buffer, frameSize?: number): Buffer; + decode(buffer: Buffer): Buffer; + decode(buffer: Buffer, frameSize: number): Buffer; + decode(buffer: Buffer, frameSize?: number): Buffer; + applyEncoderCTL?(ctl: number, value: number): void; + encoderCTL?(ctl: number, value: number): void; + delete?(): void; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Application?: any; +}; + +type IMod = [ + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (mod: any) => { + Encoder: IEncoder; + } +]; + +const loadModule = ( + modules: IMod[] +): { + Encoder: IEncoder; + name: string; +} => { + const errors: string[] = []; + + for (const [name, fn] of modules) { + try { + return { + // eslint-disable-next-line @typescript-eslint/no-var-requires + ...fn(require(name)), + name + }; + } catch (e) { + errors.push(`Failed to load ${name}: ${e}`); + continue; + } + } + + throw new Error(`Could not load opus module, tried ${modules.length} different modules. Errors: ${errors.join('\n')}`); +}; + +export const CTL = { + BITRATE: 0xfa2, + FEC: 0xfac, + PLP: 0xfae +} as const; + +const OPUS_MOD_REGISTRY: IMod[] = [ + [ + 'mediaplex', + (mod) => { + if (!mod.OpusEncoder) throw new Error('Unsupported mediaplex version'); + return { Encoder: mod.OpusEncoder }; + } + ], + ['@discordjs/opus', (opus) => ({ Encoder: opus.OpusEncoder })], + ['opusscript', (opus) => ({ Encoder: opus })], + [ + '@evan/opus', + (opus) => { + const { Encoder, Decoder } = opus as typeof import('@evan/opus'); + + class OpusEncoder { + private _encoder!: InstanceType | null; + private _decoder!: InstanceType | null; + + public constructor(private _rate: number, private _channels: number, private _application: number) {} + + private _ensureEncoder() { + if (this._encoder) return; + this._encoder = new Encoder({ + channels: this._channels as 2, + sample_rate: this._rate as 48000, + application: ({ + 2048: 'voip', + 2049: 'audio', + 2051: 'restricted_lowdelay' + })[this._application] + }); + } + + private _ensureDecoder() { + if (this._decoder) return; + this._decoder = new Decoder({ + channels: this._channels as 2, + sample_rate: this._rate as 48000 + }); + } + + public encode(buffer: Buffer) { + this._ensureEncoder(); + return Buffer.from(this._encoder!.encode(buffer)); + } + + public decode(buffer: Buffer) { + this._ensureDecoder(); + return Buffer.from(this._decoder!.decode(buffer)); + } + + public applyEncoderCTL(ctl: number, value: number) { + this._ensureEncoder(); + this._encoder!.ctl(ctl, value); + } + + public delete() { + this._encoder = null; + this._decoder = null; + } + } + + return { Encoder: OpusEncoder }; + } + ], + ['node-opus', (opus) => ({ Encoder: opus.OpusEncoder })] +]; + +let Opus: { Encoder?: IEncoder; name?: string } = {}; + +/** + * Add a new Opus provider to the registry. This will be tried to load in order at runtime. + * @param provider - The provider to add + */ +export const addLibopusProvider = (provider: IMod) => { + if (OPUS_MOD_REGISTRY.some(([, fn]) => fn === provider[1])) return; + OPUS_MOD_REGISTRY.push(provider); +}; + +/** + * Remove an Opus provider from the registry. + * @param name - The name of the provider to remove + */ +export const removeLibopusProvider = (name: string) => { + const index = OPUS_MOD_REGISTRY.findIndex((o) => o[0] === name); + if (index === -1) return false; + OPUS_MOD_REGISTRY.splice(index, 1); + return true; +}; + +/** + * Set the Opus provider to use. This will override the automatic provider selection. + * @param provider - The provider to use + */ +export const setLibopusProvider = (provider: IEncoder, name: string) => { + Opus = { Encoder: provider, name }; +}; + +function loadOpus(refresh = false) { + if (Opus.Encoder && !refresh) return Opus; + + Opus = loadModule(OPUS_MOD_REGISTRY); + return Opus; +} + +const charCode = (x: string) => x.charCodeAt(0); +const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode)); +const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode)); + +export interface IOpusStreamInit { + frameSize: number; + channels: number; + rate: number; + application?: number; +} + +// frame size = (channels * rate * frame_duration) / 1000 + +/** + * Takes a stream of Opus data and outputs a stream of PCM data, or the inverse. + * **You shouldn't directly instantiate this class, see opus.Encoder and opus.Decoder instead!** + * @memberof opus + * @extends TransformStream + * @protected + */ +export class OpusStream extends Transform { + public encoder: InstanceType | null = null; + public _options: IOpusStreamInit; + public _required: number; + /** + * Creates a new Opus transformer. + * @private + * @memberof opus + * @param {Object} [options] options that you would pass to a regular Transform stream + */ + constructor(options = {} as IOpusStreamInit) { + if (!loadOpus().Encoder) { + throw Error(`Could not find an Opus module! Please install one of ${OPUS_MOD_REGISTRY.map((o) => o[0]).join(', ')}.`); + } + super(Object.assign({ readableObjectMode: true }, options)); + + const lib = Opus as Required; + + if (lib.name === 'opusscript') { + options.application = lib.Encoder.Application![options.application!]; + } + + this.encoder = new lib.Encoder(options.rate, options.channels, options.application!); + + this._options = options; + this._required = this._options.frameSize * this._options.channels * 2; + } + + _encode(buffer: Buffer) { + if (Opus.name === 'opusscript') { + return this.encoder!.encode(buffer, this._options.frameSize); + } else { + return this.encoder!.encode(buffer); + } + } + + _decode(buffer: Buffer) { + if (Opus.name === 'opusscript') { + return this.encoder!.decode(buffer, this._options.frameSize); + } else { + return this.encoder!.decode(buffer); + } + } + + /** + * Returns the Opus module being used - `mediaplex`, `opusscript`, `node-opus`, or `@discordjs/opus`. + * @type {string} + * @readonly + * @example + * console.log(`Using Opus module ${prism.opus.Encoder.type}`); + */ + static get type() { + return Opus.name; + } + + /** + * Sets the bitrate of the stream. + * @param {number} bitrate the bitrate to use use, e.g. 48000 + * @public + */ + setBitrate(bitrate: number) { + (this.encoder!.applyEncoderCTL! || this.encoder!.encoderCTL).apply(this.encoder!, [CTL.BITRATE, Math.min(128e3, Math.max(16e3, bitrate))]); + } + + /** + * Enables or disables forward error correction. + * @param {boolean} enabled whether or not to enable FEC. + * @public + */ + setFEC(enabled: boolean) { + (this.encoder!.applyEncoderCTL! || this.encoder!.encoderCTL).apply(this.encoder!, [CTL.FEC, enabled ? 1 : 0]); + } + + /** + * Sets the expected packet loss over network transmission. + * @param {number} [percentage] a percentage (represented between 0 and 1) + */ + setPLP(percentage: number) { + (this.encoder!.applyEncoderCTL! || this.encoder!.encoderCTL).apply(this.encoder!, [CTL.PLP, Math.min(100, Math.max(0, percentage * 100))]); + } + + _final(cb: () => void) { + this._cleanup(); + cb(); + } + + _destroy(err: Error | null, cb: (err: Error | null) => void) { + this._cleanup(); + return cb ? cb(err) : undefined; + } + + /** + * Cleans up the Opus stream when it is no longer needed + * @private + */ + _cleanup() { + if (typeof this.encoder?.delete === 'function') this.encoder!.delete!(); + this.encoder = null; + } +} + +/** + * An Opus encoder stream. + * + * Outputs opus packets in [object mode.](https://nodejs.org/api/stream.html#stream_object_mode) + * @extends opus.OpusStream + * @memberof opus + * @example + * const encoder = new prism.opus.Encoder({ frameSize: 960, channels: 2, rate: 48000 }); + * pcmAudio.pipe(encoder); + * // encoder will now output Opus-encoded audio packets + */ +export class OpusEncoder extends OpusStream { + _buffer: Buffer = Buffer.allocUnsafe(0); + + /** + * Creates a new Opus encoder stream. + * @memberof opus + * @param {Object} options options that you would pass to a regular OpusStream, plus a few more: + * @param {number} options.frameSize the frame size in bytes to use (e.g. 960 for stereo audio at 48KHz with a frame + * duration of 20ms) + * @param {number} options.channels the number of channels to use + * @param {number} options.rate the sampling rate in Hz + */ + constructor(options = {} as IOpusStreamInit) { + super(options); + } + + public _transform(newChunk: Buffer, encoding: BufferEncoding, done: TransformCallback): void { + const chunk = Buffer.concat([this._buffer, newChunk]); + + let i = 0; + while (chunk.length >= i + this._required) { + const pcm = chunk.slice(i, i + this._required); + let opus: Buffer | undefined; + try { + opus = this.encoder!.encode(pcm); + } catch (error) { + done(error as Error); + return; + } + this.push(opus); + i += this._required; + } + + if (i > 0) this._buffer = chunk.slice(i); + done(); + } + + _destroy(err: Error, cb: (err: Error | null) => void) { + super._destroy(err, cb); + this._buffer = Buffer.allocUnsafe(0); + } +} + +/** + * An Opus decoder stream. + * + * Note that any stream you pipe into this must be in + * [object mode](https://nodejs.org/api/stream.html#stream_object_mode) and should output Opus packets. + * @extends opus.OpusStream + * @memberof opus + * @example + * const decoder = new prism.opus.Decoder({ frameSize: 960, channels: 2, rate: 48000 }); + * input.pipe(decoder); + * // decoder will now output PCM audio + */ +export class OpusDecoder extends OpusStream { + _transform(chunk: Buffer, encoding: BufferEncoding, done: (e?: Error | null, chunk?: Buffer) => void) { + const signature = chunk.slice(0, 8); + if (chunk.length >= 8 && signature.equals(OPUS_HEAD)) { + this.emit('format', { + channels: this._options.channels, + sampleRate: this._options.rate, + bitDepth: 16, + float: false, + signed: true, + version: chunk.readUInt8(8), + preSkip: chunk.readUInt16LE(10), + gain: chunk.readUInt16LE(16) + }); + return done(); + } + if (chunk.length >= 8 && signature.equals(OPUS_TAGS)) { + this.emit('tags', chunk); + return done(); + } + try { + this.push(this._decode(chunk)); + } catch (e) { + return done(e as Error); + } + return done(); + } +} diff --git a/packages/opus/src/WebmBase.ts b/packages/opus/src/WebmBase.ts new file mode 100644 index 0000000000..fdcf1ff5fe --- /dev/null +++ b/packages/opus/src/WebmBase.ts @@ -0,0 +1,220 @@ +// based on https://github.com/amishshah/prism-media/blob/4ef1d6f9f53042c085c1f68627e889003e248d77/src/core/WebmBase.js + +import { Transform, TransformCallback } from 'node:stream'; + +export class WebmBaseDemuxer extends Transform { + public static readonly TAGS = { + // value is true if the element has children + '1a45dfa3': true, // EBML + '18538067': true, // Segment + '1f43b675': true, // Cluster + '1654ae6b': true, // Tracks + ae: true, // TrackEntry + d7: false, // TrackNumber + '83': false, // TrackType + a3: false, // SimpleBlock + '63a2': false + }; + + public static readonly TOO_SHORT = Symbol('TOO_SHORT'); + + private _remainder: Buffer | null = null; + private _length = 0; + private _count = 0; + private _skipUntil: number | null = null; + private _track: { number: number; type: number } | null = null; + private _incompleteTrack: { number?: number; type?: number } = {}; + private _ebmlFound = false; + + /** + * Creates a new Webm demuxer. + * @param {Object} [options] options that you would pass to a regular Transform stream. + */ + constructor(options = {}) { + super(Object.assign({ readableObjectMode: true }, options)); + this._remainder = null; + this._length = 0; + this._count = 0; + this._skipUntil = null; + this._track = null; + this._incompleteTrack = {}; + this._ebmlFound = false; + } + + public _checkHead(data: Buffer) { + void data; + } + + _transform(chunk: Buffer, encoding: BufferEncoding, done: TransformCallback) { + this._length += chunk.length; + if (this._remainder) { + chunk = Buffer.concat([this._remainder, chunk]); + this._remainder = null; + } + let offset = 0; + if (this._skipUntil && this._length > this._skipUntil) { + offset = this._skipUntil - this._count; + this._skipUntil = null; + } else if (this._skipUntil) { + this._count += chunk.length; + done(); + return; + } + + let result; + // @ts-ignore + while (result !== WebmBaseDemuxer.TOO_SHORT) { + try { + result = this._readTag(chunk, offset); + } catch (error) { + done(error as Error); + return; + } + if (result === WebmBaseDemuxer.TOO_SHORT) break; + if (result._skipUntil) { + this._skipUntil = result._skipUntil; + break; + } + if (result.offset) offset = result.offset; + else break; + } + this._count += offset; + this._remainder = chunk.subarray(offset); + done(); + return; + } + + /** + * Reads an EBML ID from a buffer. + * @private + * @param {Buffer} chunk the buffer to read from. + * @param {number} offset the offset in the buffer. + * @returns {Object|Symbol} contains an `id` property (buffer) and the new `offset` (number). + * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. + */ + _readEBMLId(chunk: Buffer, offset: number) { + const idLength = vintLength(chunk, offset); + if (idLength === WebmBaseDemuxer.TOO_SHORT) return WebmBaseDemuxer.TOO_SHORT; + return { + id: chunk.subarray(offset, offset + idLength), + offset: offset + idLength + }; + } + + /** + * Reads a size variable-integer to calculate the length of the data of a tag. + * @private + * @param {Buffer} chunk the buffer to read from. + * @param {number} offset the offset in the buffer. + * @returns {Object|Symbol} contains property `offset` (number), `dataLength` (number) and `sizeLength` (number). + * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. + */ + _readTagDataSize(chunk: Buffer, offset: number) { + const sizeLength = vintLength(chunk, offset); + if (sizeLength === WebmBaseDemuxer.TOO_SHORT) return WebmBaseDemuxer.TOO_SHORT; + const dataLength = expandVint(chunk, offset, offset + sizeLength); + return { offset: offset + sizeLength, dataLength, sizeLength }; + } + + /** + * Takes a buffer and attempts to read and process a tag. + * @private + * @param {Buffer} chunk the buffer to read from. + * @param {number} offset the offset in the buffer. + * @returns {Object|Symbol} contains the new `offset` (number) and optionally the `_skipUntil` property, + * indicating that the stream should ignore any data until a certain length is reached. + * Returns the TOO_SHORT symbol if the data wasn't big enough to facilitate the request. + */ + _readTag(chunk: Buffer, offset: number) { + const idData = this._readEBMLId(chunk, offset); + if (idData === WebmBaseDemuxer.TOO_SHORT) return WebmBaseDemuxer.TOO_SHORT; + const ebmlID = idData.id.toString('hex'); + if (!this._ebmlFound) { + if (ebmlID === '1a45dfa3') this._ebmlFound = true; + else throw Error('Did not find the EBML tag at the start of the stream'); + } + offset = idData.offset; + const sizeData = this._readTagDataSize(chunk, offset); + if (sizeData === WebmBaseDemuxer.TOO_SHORT) return WebmBaseDemuxer.TOO_SHORT; + const { dataLength } = sizeData; + offset = sizeData.offset; + // If this tag isn't useful, tell the stream to stop processing data until the tag ends + if (typeof WebmBaseDemuxer.TAGS[ebmlID as keyof (typeof WebmBaseDemuxer)['TAGS']] === 'undefined') { + if (chunk.length > offset + (dataLength as number)) { + return { offset: offset + (dataLength as number) }; + } + return { offset, _skipUntil: this._count + (offset as number) + (dataLength as number) }; + } + + const tagHasChildren = WebmBaseDemuxer.TAGS[ebmlID as keyof (typeof WebmBaseDemuxer)['TAGS']]; + if (tagHasChildren) { + return { offset }; + } + + if ((offset as number) + (dataLength as number) > chunk.length) return WebmBaseDemuxer.TOO_SHORT; + const data = chunk.subarray(offset, (offset as number) + (dataLength as number)); + if (!this._track) { + if (ebmlID === 'ae') this._incompleteTrack = {}; + if (ebmlID === 'd7') this._incompleteTrack.number = data[0]; + if (ebmlID === '83') this._incompleteTrack.type = data[0]; + if (this._incompleteTrack.type === 2 && typeof this._incompleteTrack.number !== 'undefined') { + // @ts-ignore + this._track = this._incompleteTrack; + } + } + if (ebmlID === '63a2') { + this._checkHead(data); + this.emit('head', data); + } else if (ebmlID === 'a3') { + if (!this._track) throw Error('No audio track in this webm!'); + if ((data[0] & 0xf) === this._track.number) { + this.push(data.subarray(4)); + } + } + return { offset: (offset as number) + (dataLength as number) }; + } + + _destroy(err: Error, cb: (error: Error | null) => void) { + this._cleanup(); + return cb ? cb(err) : undefined; + } + + _final(cb: TransformCallback) { + this._cleanup(); + cb(); + } + + /** + * Cleans up the demuxer when it is no longer required. + * @private + */ + _cleanup() { + this._remainder = null; + this._incompleteTrack = {}; + } +} + +function vintLength(buffer: Buffer, index: number) { + if (index < 0 || index > buffer.length - 1) { + return WebmBaseDemuxer.TOO_SHORT; + } + let i = 0; + for (; i < 8; i++) if ((1 << (7 - i)) & buffer[index]) break; + i++; + if (index + i > buffer.length) { + return WebmBaseDemuxer.TOO_SHORT; + } + return i; +} + +function expandVint(buffer: Buffer, start: number, end: number) { + const length = vintLength(buffer, start); + if (end > buffer.length || length === WebmBaseDemuxer.TOO_SHORT) return WebmBaseDemuxer.TOO_SHORT; + // @ts-ignore + const mask = (1 << (8 - length)) - 1; + let value = buffer[start] & mask; + for (let i = start + 1; i < end; i++) { + value = (value << 8) + buffer[i]; + } + return value; +} diff --git a/packages/opus/src/WebmDemuxer.ts b/packages/opus/src/WebmDemuxer.ts new file mode 100644 index 0000000000..bb529c3a2a --- /dev/null +++ b/packages/opus/src/WebmDemuxer.ts @@ -0,0 +1,22 @@ +// based on https://github.com/amishshah/prism-media/blob/4ef1d6f9f53042c085c1f68627e889003e248d77/src/opus/WebmDemuxer.js + +import { WebmBaseDemuxer } from './WebmBase'; + +const OPUS_HEAD = Buffer.from([...'OpusHead'].map((x) => x.charCodeAt(0))); + +/** + * Demuxes a Webm stream (containing Opus audio) to output an Opus stream. + * @example + * const fs = require('fs'); + * const file = fs.createReadStream('./audio.webm'); + * const demuxer = new prism.opus.WebmDemuxer(); + * const opus = file.pipe(demuxer); + * // opus is now a ReadableStream in object mode outputting Opus packets + */ +export class WebmDemuxer extends WebmBaseDemuxer { + _checkHead(data: Buffer) { + if (!data.subarray(0, 8).equals(OPUS_HEAD)) { + throw Error('Audio codec is not Opus!'); + } + } +} diff --git a/packages/opus/src/index.ts b/packages/opus/src/index.ts index db8e7c3edd..6b6bbc94e2 100644 --- a/packages/opus/src/index.ts +++ b/packages/opus/src/index.ts @@ -1,354 +1,6 @@ -// based on https://github.com/amishshah/prism-media/blob/4ef1d6f9f53042c085c1f68627e889003e248d77/src/opus/Opus.js - -import { Transform, type TransformCallback } from 'stream'; - -export type IEncoder = { - new (rate: number, channels: number, application: number): { - encode(buffer: Buffer): Buffer; - encode(buffer: Buffer, frameSize: number): Buffer; - encode(buffer: Buffer, frameSize?: number): Buffer; - decode(buffer: Buffer): Buffer; - decode(buffer: Buffer, frameSize: number): Buffer; - decode(buffer: Buffer, frameSize?: number): Buffer; - applyEncoderCTL?(ctl: number, value: number): void; - encoderCTL?(ctl: number, value: number): void; - delete?(): void; - }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Application?: any; -}; - -type IMod = [ - string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (mod: any) => { - Encoder: IEncoder; - } -]; - -const loadModule = ( - modules: IMod[] -): { - Encoder: IEncoder; - name: string; -} => { - const errors: string[] = []; - - for (const [name, fn] of modules) { - try { - return { - // eslint-disable-next-line @typescript-eslint/no-var-requires - ...fn(require(name)), - name - }; - } catch (e) { - errors.push(`Failed to load ${name}: ${e}`); - continue; - } - } - - throw new Error(`Could not load opus module, tried ${modules.length} different modules. Errors: ${errors.join('\n')}`); -}; - -export const CTL = { - BITRATE: 0xfa2, - FEC: 0xfac, - PLP: 0xfae -} as const; - -export const OPUS_MOD_REGISTRY: IMod[] = [ - [ - 'mediaplex', - (mod) => { - if (!mod.OpusEncoder) throw new Error('Unsupported mediaplex version'); - return { Encoder: mod.OpusEncoder }; - } - ], - ['@discordjs/opus', (opus) => ({ Encoder: opus.OpusEncoder })], - ['opusscript', (opus) => ({ Encoder: opus })], - [ - '@evan/opus', - (opus) => { - const { Encoder, Decoder } = opus as typeof import('@evan/opus'); - - class OpusEncoder { - private _encoder!: InstanceType | null; - private _decoder!: InstanceType | null; - - public constructor(private _rate: number, private _channels: number, private _application: number) {} - - private _ensureEncoder() { - if (this._encoder) return; - this._encoder = new Encoder({ - channels: this._channels as 2, - sample_rate: this._rate as 48000, - application: ({ - 2048: 'voip', - 2049: 'audio', - 2051: 'restricted_lowdelay' - })[this._application] - }); - } - - private _ensureDecoder() { - if (this._decoder) return; - this._decoder = new Decoder({ - channels: this._channels as 2, - sample_rate: this._rate as 48000 - }); - } - - public encode(buffer: Buffer) { - this._ensureEncoder(); - return Buffer.from(this._encoder!.encode(buffer)); - } - - public decode(buffer: Buffer) { - this._ensureDecoder(); - return Buffer.from(this._decoder!.decode(buffer)); - } - - public applyEncoderCTL(ctl: number, value: number) { - this._ensureEncoder(); - this._encoder!.ctl(ctl, value); - } - - public delete() { - this._encoder = null; - this._decoder = null; - } - } - - return { Encoder: OpusEncoder }; - } - ], - ['node-opus', (opus) => ({ Encoder: opus.OpusEncoder })] -]; - -let Opus: { Encoder?: IEncoder; name?: string } = {}; - -function loadOpus(refresh = false) { - if (Opus.Encoder && !refresh) return Opus; - - Opus = loadModule(OPUS_MOD_REGISTRY); - return Opus; -} - -const charCode = (x: string) => x.charCodeAt(0); -const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode)); -const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode)); - -export interface IOpusStreamInit { - frameSize: number; - channels: number; - rate: number; - application?: number; -} - -// frame size = (channels * rate * frame_duration) / 1000 - -/** - * Takes a stream of Opus data and outputs a stream of PCM data, or the inverse. - * **You shouldn't directly instantiate this class, see opus.Encoder and opus.Decoder instead!** - * @memberof opus - * @extends TransformStream - * @protected - */ -export class OpusStream extends Transform { - public encoder: InstanceType | null = null; - public _options: IOpusStreamInit; - public _required: number; - /** - * Creates a new Opus transformer. - * @private - * @memberof opus - * @param {Object} [options] options that you would pass to a regular Transform stream - */ - constructor(options = {} as IOpusStreamInit) { - if (!loadOpus().Encoder) { - throw Error(`Could not find an Opus module! Please install one of ${OPUS_MOD_REGISTRY.map((o) => o[0]).join(', ')}.`); - } - super(Object.assign({ readableObjectMode: true }, options)); - - const lib = Opus as Required; - - if (lib.name === 'opusscript') { - options.application = lib.Encoder.Application![options.application!]; - } - - this.encoder = new lib.Encoder(options.rate, options.channels, options.application!); - - this._options = options; - this._required = this._options.frameSize * this._options.channels * 2; - } - - _encode(buffer: Buffer) { - if (Opus.name === 'opusscript') { - return this.encoder!.encode(buffer, this._options.frameSize); - } else { - return this.encoder!.encode(buffer); - } - } - - _decode(buffer: Buffer) { - if (Opus.name === 'opusscript') { - return this.encoder!.decode(buffer, this._options.frameSize); - } else { - return this.encoder!.decode(buffer); - } - } - - /** - * Returns the Opus module being used - `mediaplex`, `opusscript`, `node-opus`, or `@discordjs/opus`. - * @type {string} - * @readonly - * @example - * console.log(`Using Opus module ${prism.opus.Encoder.type}`); - */ - static get type() { - return Opus.name; - } - - /** - * Sets the bitrate of the stream. - * @param {number} bitrate the bitrate to use use, e.g. 48000 - * @public - */ - setBitrate(bitrate: number) { - (this.encoder!.applyEncoderCTL! || this.encoder!.encoderCTL).apply(this.encoder!, [CTL.BITRATE, Math.min(128e3, Math.max(16e3, bitrate))]); - } - - /** - * Enables or disables forward error correction. - * @param {boolean} enabled whether or not to enable FEC. - * @public - */ - setFEC(enabled: boolean) { - (this.encoder!.applyEncoderCTL! || this.encoder!.encoderCTL).apply(this.encoder!, [CTL.FEC, enabled ? 1 : 0]); - } - - /** - * Sets the expected packet loss over network transmission. - * @param {number} [percentage] a percentage (represented between 0 and 1) - */ - setPLP(percentage: number) { - (this.encoder!.applyEncoderCTL! || this.encoder!.encoderCTL).apply(this.encoder!, [CTL.PLP, Math.min(100, Math.max(0, percentage * 100))]); - } - - _final(cb: () => void) { - this._cleanup(); - cb(); - } - - _destroy(err: Error | null, cb: (err: Error | null) => void) { - this._cleanup(); - return cb ? cb(err) : undefined; - } - - /** - * Cleans up the Opus stream when it is no longer needed - * @private - */ - _cleanup() { - if (typeof this.encoder?.delete === 'function') this.encoder!.delete!(); - this.encoder = null; - } -} - -/** - * An Opus encoder stream. - * - * Outputs opus packets in [object mode.](https://nodejs.org/api/stream.html#stream_object_mode) - * @extends opus.OpusStream - * @memberof opus - * @example - * const encoder = new prism.opus.Encoder({ frameSize: 960, channels: 2, rate: 48000 }); - * pcmAudio.pipe(encoder); - * // encoder will now output Opus-encoded audio packets - */ -export class OpusEncoder extends OpusStream { - _buffer: Buffer = Buffer.allocUnsafe(0); - - /** - * Creates a new Opus encoder stream. - * @memberof opus - * @param {Object} options options that you would pass to a regular OpusStream, plus a few more: - * @param {number} options.frameSize the frame size in bytes to use (e.g. 960 for stereo audio at 48KHz with a frame - * duration of 20ms) - * @param {number} options.channels the number of channels to use - * @param {number} options.rate the sampling rate in Hz - */ - constructor(options = {} as IOpusStreamInit) { - super(options); - } - - public _transform(newChunk: Buffer, encoding: BufferEncoding, done: TransformCallback): void { - const chunk = Buffer.concat([this._buffer, newChunk]); - - let i = 0; - while (chunk.length >= i + this._required) { - const pcm = chunk.slice(i, i + this._required); - let opus: Buffer | undefined; - try { - opus = this.encoder!.encode(pcm); - } catch (error) { - done(error as Error); - return; - } - this.push(opus); - i += this._required; - } - - if (i > 0) this._buffer = chunk.slice(i); - done(); - } - - _destroy(err: Error, cb: (err: Error | null) => void) { - super._destroy(err, cb); - this._buffer = Buffer.allocUnsafe(0); - } -} - -/** - * An Opus decoder stream. - * - * Note that any stream you pipe into this must be in - * [object mode](https://nodejs.org/api/stream.html#stream_object_mode) and should output Opus packets. - * @extends opus.OpusStream - * @memberof opus - * @example - * const decoder = new prism.opus.Decoder({ frameSize: 960, channels: 2, rate: 48000 }); - * input.pipe(decoder); - * // decoder will now output PCM audio - */ -export class OpusDecoder extends OpusStream { - _transform(chunk: Buffer, encoding: BufferEncoding, done: (e?: Error | null, chunk?: Buffer) => void) { - const signature = chunk.slice(0, 8); - if (chunk.length >= 8 && signature.equals(OPUS_HEAD)) { - this.emit('format', { - channels: this._options.channels, - sampleRate: this._options.rate, - bitDepth: 16, - float: false, - signed: true, - version: chunk.readUInt8(8), - preSkip: chunk.readUInt16LE(10), - gain: chunk.readUInt16LE(16) - }); - return done(); - } - if (chunk.length >= 8 && signature.equals(OPUS_TAGS)) { - this.emit('tags', chunk); - return done(); - } - try { - this.push(this._decode(chunk)); - } catch (e) { - return done(e as Error); - } - return done(); - } -} +export * from './OggDemuxer'; +export * from './OpusEncoder'; +export * from './WebmDemuxer'; // eslint-disable-next-line @typescript-eslint/no-inferrable-types export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/yarn.lock b/yarn.lock index ac743124d2..ca9d7f9355 100644 --- a/yarn.lock +++ b/yarn.lock @@ -125,6 +125,15 @@ __metadata: languageName: unknown linkType: soft +"@discord-player/node@workspace:^, @discord-player/node@workspace:packages/node": + version: 0.0.0-use.local + resolution: "@discord-player/node@workspace:packages/node" + dependencies: + "@discord-player/tsconfig": "workspace:^" + tsup: "npm:^8.1.0" + languageName: unknown + linkType: soft + "@discord-player/opus@npm:^0.1.2, @discord-player/opus@workspace:packages/opus": version: 0.0.0-use.local resolution: "@discord-player/opus@workspace:packages/opus" @@ -254,6 +263,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm64@npm:0.18.20" @@ -268,6 +284,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm@npm:0.18.20" @@ -282,6 +305,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-x64@npm:0.18.20" @@ -296,6 +326,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-arm64@npm:0.18.20" @@ -310,6 +347,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-x64@npm:0.18.20" @@ -324,6 +368,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-arm64@npm:0.18.20" @@ -338,6 +389,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-x64@npm:0.18.20" @@ -352,6 +410,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm64@npm:0.18.20" @@ -366,6 +431,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm@npm:0.18.20" @@ -380,6 +452,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ia32@npm:0.18.20" @@ -394,6 +473,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-loong64@npm:0.18.20" @@ -408,6 +494,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-mips64el@npm:0.18.20" @@ -422,6 +515,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ppc64@npm:0.18.20" @@ -436,6 +536,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-riscv64@npm:0.18.20" @@ -450,6 +557,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-s390x@npm:0.18.20" @@ -464,6 +578,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-x64@npm:0.18.20" @@ -478,6 +599,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/netbsd-x64@npm:0.18.20" @@ -492,6 +620,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/openbsd-x64@npm:0.18.20" @@ -506,6 +641,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/sunos-x64@npm:0.18.20" @@ -520,6 +662,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-arm64@npm:0.18.20" @@ -534,6 +683,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-ia32@npm:0.18.20" @@ -548,6 +704,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-x64@npm:0.18.20" @@ -562,6 +725,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -2814,6 +2984,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.18.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-android-arm64@npm:4.1.4" @@ -2821,6 +2998,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-android-arm64@npm:4.18.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-darwin-arm64@npm:4.1.4" @@ -2828,6 +3012,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.18.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-darwin-x64@npm:4.1.4" @@ -2835,6 +3026,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.18.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.1.4" @@ -2842,6 +3040,20 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.18.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.18.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.1.4" @@ -2849,6 +3061,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.18.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.1.4" @@ -2856,6 +3075,34 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.18.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.18.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.18.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.18.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.1.4" @@ -2863,6 +3110,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.18.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-linux-x64-musl@npm:4.1.4" @@ -2870,6 +3124,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.18.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.1.4" @@ -2877,6 +3138,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.18.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.1.4" @@ -2884,6 +3152,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.18.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.1.4": version: 4.1.4 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.1.4" @@ -2891,6 +3166,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.18.0": + version: 4.18.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.18.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rushstack/eslint-patch@npm:^1.1.3": version: 1.3.2 resolution: "@rushstack/eslint-patch@npm:1.3.2" @@ -3093,6 +3375,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.5": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: 10/7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 + languageName: node + linkType: hard + "@types/hast@npm:^2.0.0": version: 2.3.4 resolution: "@types/hast@npm:2.3.4" @@ -4964,6 +5253,7 @@ __metadata: dependencies: "@discord-player/equalizer": "workspace:^" "@discord-player/ffmpeg": "workspace:^" + "@discord-player/node": "workspace:^" "@discord-player/tsconfig": "workspace:^" "@discord-player/utils": "workspace:^" "@types/ip": "npm:^1.1.0" @@ -4998,6 +5288,16 @@ __metadata: languageName: node linkType: hard +"discord-voip@workspace:packages/discord-voip": + version: 0.0.0-use.local + resolution: "discord-voip@workspace:packages/discord-voip" + dependencies: + "@discord-player/tsconfig": "workspace:^" + tsup: "npm:^7.2.0" + typescript: "npm:^5.2.2" + languageName: unknown + linkType: soft + "discord.js@npm:^14.1.2": version: 14.1.2 resolution: "discord.js@npm:14.1.2" @@ -5428,6 +5728,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.4": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/d2ff2ca84d30cce8e871517374d6c2290835380dc7cd413b2d49189ed170d45e407be14de2cb4794cf76f75cf89955c4714726ebd3de7444b3046f5cab23ab6b + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -10591,6 +10971,69 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.0.2": + version: 4.18.0 + resolution: "rollup@npm:4.18.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.18.0" + "@rollup/rollup-android-arm64": "npm:4.18.0" + "@rollup/rollup-darwin-arm64": "npm:4.18.0" + "@rollup/rollup-darwin-x64": "npm:4.18.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.18.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.18.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.18.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.18.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.18.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.18.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.18.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.18.0" + "@rollup/rollup-linux-x64-musl": "npm:4.18.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.18.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.18.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.18.0" + "@types/estree": "npm:1.0.5" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10/2320fe653cfd5e3d72ecab2f1d52d47e7b624a6ab02919f53c1ad1c5efa3b66e277c3ecfef03bb97651e79cef04bfefd34ad1f6e648f496572bf76c834f19599 + languageName: node + linkType: hard + "rollup@npm:^4.1.4": version: 4.1.4 resolution: "rollup@npm:4.1.4" @@ -11672,6 +12115,45 @@ __metadata: languageName: node linkType: hard +"tsup@npm:^8.1.0": + version: 8.1.0 + resolution: "tsup@npm:8.1.0" + dependencies: + bundle-require: "npm:^4.0.0" + cac: "npm:^6.7.12" + chokidar: "npm:^3.5.1" + debug: "npm:^4.3.1" + esbuild: "npm:^0.21.4" + execa: "npm:^5.0.0" + globby: "npm:^11.0.3" + joycon: "npm:^3.0.1" + postcss-load-config: "npm:^4.0.1" + resolve-from: "npm:^5.0.0" + rollup: "npm:^4.0.2" + source-map: "npm:0.8.0-beta.0" + sucrase: "npm:^3.20.3" + tree-kill: "npm:^1.2.2" + peerDependencies: + "@microsoft/api-extractor": ^7.36.0 + "@swc/core": ^1 + postcss: ^8.4.12 + typescript: ">=4.5.0" + peerDependenciesMeta: + "@microsoft/api-extractor": + optional: true + "@swc/core": + optional: true + postcss: + optional: true + typescript: + optional: true + bin: + tsup: dist/cli-default.js + tsup-node: dist/cli-node.js + checksum: 10/5a575e8d45eb91b7a0850fa554166a8a1f047b35601bfc0eb2cd04804403bf1eef8a9799207748efe10e35da748a79da7546124a253ee07c6b27753a64b04bcc + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" From 4cfa76983346fc106193e21eb644a516991bd780 Mon Sep 17 00:00:00 2001 From: Twilight <46562212+twlite@users.noreply.github.com> Date: Sat, 29 Jun 2024 20:39:05 +0545 Subject: [PATCH 4/7] feat(DependencyReport): add discord-voip --- packages/discord-player/src/utils/DependencyReport.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/discord-player/src/utils/DependencyReport.ts b/packages/discord-player/src/utils/DependencyReport.ts index 88e50d40af..44cb25b9d9 100644 --- a/packages/discord-player/src/utils/DependencyReport.ts +++ b/packages/discord-player/src/utils/DependencyReport.ts @@ -11,6 +11,7 @@ export type MaybeNull = T | null; export interface DependenciesReport { core: { 'discord-player': string; + 'discord-voip': string; }; libopus: { mediaplex: MaybeNull; @@ -108,7 +109,8 @@ export const DependencyReport = { return { core: { - 'discord-player': DependencyReport.version('discord-player') as string + 'discord-player': DependencyReport.version('discord-player') as string, + 'discord-voip': DependencyReport.version('discord-voip') as string }, libopus: { mediaplex: DependencyReport.version('mediaplex'), From cda058a722508c761955bb3e28bcf9100d23e04c Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 30 Jun 2024 14:01:33 +0545 Subject: [PATCH 5/7] fix: remove prism media --- apps/docs/package.json | 4 + packages/discord-player/package.json | 17 +- packages/discord-player/tsconfig.json | 2 +- packages/discord-voip/LICENSE | 21 + packages/discord-voip/package.json | 7 +- packages/discord-voip/scripts/esm-shim.cjs | 15 + .../discord-voip/src/audio/AudioResource.ts | 21 +- .../src/audio/TransformerGraph.ts | 2 +- packages/discord-voip/src/util/demuxProbe.ts | 128 ------ packages/discord-voip/src/util/index.ts | 1 - packages/discord-voip/tsconfig.json | 4 + packages/discord-voip/tsup.config.ts | 7 + packages/discord-voip/typedoc.json | 5 + packages/equalizer/package.json | 4 +- packages/ffmpeg/package.json | 4 +- packages/node/package.json | 3 +- packages/opus/package.json | 4 +- packages/tsconfig/package.json | 3 +- packages/utils/package.json | 4 +- yarn.lock | 399 +++--------------- 20 files changed, 138 insertions(+), 517 deletions(-) create mode 100644 packages/discord-voip/LICENSE create mode 100644 packages/discord-voip/scripts/esm-shim.cjs delete mode 100644 packages/discord-voip/src/util/demuxProbe.ts create mode 100644 packages/discord-voip/tsconfig.json create mode 100644 packages/discord-voip/tsup.config.ts create mode 100644 packages/discord-voip/typedoc.json diff --git a/apps/docs/package.json b/apps/docs/package.json index d8263ffebc..ccdbe7453b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -18,5 +18,9 @@ "homepage": "https://github.com/Androz2091/discord-player#readme", "dependencies": { "mintlify": "^4.0.160" + }, + "devDependencies": { + "tsup": "^8.1.0", + "typescript": "^5.5.2" } } diff --git a/packages/discord-player/package.json b/packages/discord-player/package.json index ca16e6f4b5..d9bc9c9966 100644 --- a/packages/discord-player/package.json +++ b/packages/discord-player/package.json @@ -53,23 +53,14 @@ "@discord-player/equalizer": "workspace:^", "@discord-player/ffmpeg": "workspace:^", "@discord-player/node": "workspace:^", - "@discord-player/utils": "workspace:^", - "@web-scrobbler/metadata-filter": "^3.1.0", - "discord-voip": "^0.1.3", - "ip": "^2.0.1", - "libsodium-wrappers": "^0.7.13", - "ws": "^8.17.0" + "@discord-player/utils": "workspace:^" }, "devDependencies": { "@discord-player/tsconfig": "workspace:^", - "@types/ip": "^1.1.0", "@types/node": "^18.6.3", - "@types/ws": "^8.5.3", - "discord-api-types": "^0.37.0", - "discord.js": "^14.1.2", - "opusscript": "^0.0.8", - "tsup": "^7.2.0", - "typescript": "^5.2.2", + "discord-api-types": "^0.37.91", + "tsup": "^8.1.0", + "typescript": "^5.5.2", "vitest": "^0.34.6" }, "typedoc": { diff --git a/packages/discord-player/tsconfig.json b/packages/discord-player/tsconfig.json index f32db5c361..f1f1679fa3 100644 --- a/packages/discord-player/tsconfig.json +++ b/packages/discord-player/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@discord-player/tsconfig/base.json", - "include": ["src/**/*", "../discord-voip"] + "include": ["src/**/*"] } diff --git a/packages/discord-voip/LICENSE b/packages/discord-voip/LICENSE new file mode 100644 index 0000000000..fe07fc7364 --- /dev/null +++ b/packages/discord-voip/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Androz2091 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/discord-voip/package.json b/packages/discord-voip/package.json index f2540419b9..d2e948e471 100644 --- a/packages/discord-voip/package.json +++ b/packages/discord-voip/package.json @@ -35,7 +35,8 @@ }, "devDependencies": { "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "@types/ws": "^8.5.10", + "tsup": "^8.1.0", + "typescript": "^5.5.2" } -} +} \ No newline at end of file diff --git a/packages/discord-voip/scripts/esm-shim.cjs b/packages/discord-voip/scripts/esm-shim.cjs new file mode 100644 index 0000000000..36bf216649 --- /dev/null +++ b/packages/discord-voip/scripts/esm-shim.cjs @@ -0,0 +1,15 @@ +/* eslint-disable */ +const { writeFileSync } = require('fs'); + +const mod = require(`${__dirname}/../dist/index.js`); + +const entries = Object.keys(mod); +const exportsMeta = entries.map((m) => `\t${m}`).join(',\n'); + +const content = [ + `import DiscordPlayer from './index.js';\n`, + `const {\n${exportsMeta}\n} = DiscordPlayer;\n`, + `export {\n${exportsMeta}\n};` +]; + +writeFileSync(`${__dirname}/../dist/index.mjs`, content.join('\n')); diff --git a/packages/discord-voip/src/audio/AudioResource.ts b/packages/discord-voip/src/audio/AudioResource.ts index f4ca9477fa..6207913be6 100644 --- a/packages/discord-voip/src/audio/AudioResource.ts +++ b/packages/discord-voip/src/audio/AudioResource.ts @@ -3,10 +3,11 @@ import type { Buffer } from 'node:buffer'; import { pipeline, type Readable } from 'node:stream'; -import prism from 'prism-media'; import { noop } from '../util/util'; import { SILENCE_FRAME, type AudioPlayer } from './AudioPlayer'; import { findPipeline, StreamType, TransformerType, type Edge } from './TransformerGraph'; +import { OggDemuxer, OpusDecoder, OpusEncoder, WebmDemuxer } from '@discord-player/opus'; +import { VolumeTransformer } from '@discord-player/equalizer'; /** * Options that are set when creating a new audio resource. @@ -66,13 +67,13 @@ export class AudioResource { * If the resource was created with inline volume transformation enabled, then this will be a * prism-media VolumeTransformer. You can use this to alter the volume of the stream. */ - public readonly volume?: prism.VolumeTransformer; + public readonly volume?: VolumeTransformer; /** * If using an Opus encoder to create this audio resource, then this will be a prism-media opus.Encoder. * You can use this to control settings such as bitrate, FEC, PLP. */ - public readonly encoder?: prism.opus.Encoder; + public readonly encoder?: OpusEncoder; /** * The audio player that the resource is subscribed to, if any. @@ -106,9 +107,9 @@ export class AudioResource { this.silencePaddingFrames = silencePaddingFrames; for (const stream of streams) { - if (stream instanceof prism.VolumeTransformer) { + if (stream instanceof VolumeTransformer) { this.volume = stream; - } else if (stream instanceof prism.opus.Encoder) { + } else if (stream instanceof OpusEncoder) { this.encoder = stream; } } @@ -183,15 +184,15 @@ export function inferStreamType(stream: Readable): { hasVolume: boolean; streamType: StreamType; } { - if (stream instanceof prism.opus.Encoder) { + if (stream instanceof OpusEncoder) { return { streamType: StreamType.Opus, hasVolume: false }; - } else if (stream instanceof prism.opus.Decoder) { + } else if (stream instanceof OpusDecoder) { return { streamType: StreamType.Raw, hasVolume: false }; - } else if (stream instanceof prism.VolumeTransformer) { + } else if (stream instanceof VolumeTransformer) { return { streamType: StreamType.Raw, hasVolume: true }; - } else if (stream instanceof prism.opus.OggDemuxer) { + } else if (stream instanceof OggDemuxer) { return { streamType: StreamType.Opus, hasVolume: false }; - } else if (stream instanceof prism.opus.WebmDemuxer) { + } else if (stream instanceof WebmDemuxer) { return { streamType: StreamType.Opus, hasVolume: false }; } diff --git a/packages/discord-voip/src/audio/TransformerGraph.ts b/packages/discord-voip/src/audio/TransformerGraph.ts index 4772ade2f9..e62ee46a13 100644 --- a/packages/discord-voip/src/audio/TransformerGraph.ts +++ b/packages/discord-voip/src/audio/TransformerGraph.ts @@ -250,7 +250,7 @@ function findPath(from: Node, constraints: (path: Edge[]) => boolean, goal = get * @param step - The first step of the path */ function constructPipeline(step: Step) { - const edges = []; + const edges: Edge[] = []; let current: Step | undefined = step; while (current?.edge) { edges.push(current.edge); diff --git a/packages/discord-voip/src/util/demuxProbe.ts b/packages/discord-voip/src/util/demuxProbe.ts deleted file mode 100644 index 34607df686..0000000000 --- a/packages/discord-voip/src/util/demuxProbe.ts +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright discord-player authors. All rights reserved. MIT License. -// Copyright discord.js authors. All rights reserved. Apache License 2.0 - -import { Buffer } from 'node:buffer'; -import process from 'node:process'; -import { Readable } from 'node:stream'; -import prism from 'prism-media'; -import { StreamType } from '..'; -import { noop } from './util'; - -/** - * Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel. - * - * @param opusHead - The Opus Head to validate - * @returns `true` if suitable to play in a Discord voice channel, otherwise `false` - */ -export function validateDiscordOpusHead(opusHead: Buffer): boolean { - const channels = opusHead.readUInt8(9); - const sampleRate = opusHead.readUInt32LE(12); - return channels === 2 && sampleRate === 48_000; -} - -/** - * The resulting information after probing an audio stream - */ -export interface ProbeInfo { - /** - * The readable audio stream to use. You should use this rather than the input stream, as the probing - * function can sometimes read the input stream to its end and cause the stream to close. - */ - stream: Readable; - - /** - * The recommended stream type for this audio stream. - */ - type: StreamType; -} - -/** - * Attempt to probe a readable stream to figure out whether it can be demuxed using an Ogg or WebM Opus demuxer. - * - * @param stream - The readable stream to probe - * @param probeSize - The number of bytes to attempt to read before giving up on the probe - * @param validator - The Opus Head validator function - * @experimental - */ -export async function demuxProbe(stream: Readable, probeSize = 1_024, validator = validateDiscordOpusHead): Promise { - return new Promise((resolve, reject) => { - // Preconditions - if (stream.readableObjectMode) { - reject(new Error('Cannot probe a readable stream in object mode')); - return; - } - - if (stream.readableEnded) { - reject(new Error('Cannot probe a stream that has ended')); - return; - } - - let readBuffer = Buffer.alloc(0); - - let resolved: StreamType | undefined; - - const finish = (type: StreamType) => { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - stream.off('data', onData); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - stream.off('close', onClose); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - stream.off('end', onClose); - stream.pause(); - resolved = type; - if (stream.readableEnded) { - resolve({ - stream: Readable.from(readBuffer), - type - }); - } else { - if (readBuffer.length > 0) { - stream.push(readBuffer); - } - - resolve({ - stream, - type - }); - } - }; - - const foundHead = (type: StreamType) => (head: Buffer) => { - if (validator(head)) { - finish(type); - } - }; - - const webm = new prism.opus.WebmDemuxer(); - webm.once('error', noop); - webm.on('head', foundHead(StreamType.WebmOpus)); - - const ogg = new prism.opus.OggDemuxer(); - ogg.once('error', noop); - ogg.on('head', foundHead(StreamType.OggOpus)); - - const onClose = () => { - if (!resolved) { - finish(StreamType.Arbitrary); - } - }; - - const onData = (buffer: Buffer) => { - readBuffer = Buffer.concat([readBuffer, buffer]); - - webm.write(buffer); - ogg.write(buffer); - - if (readBuffer.length >= probeSize) { - stream.off('data', onData); - stream.pause(); - process.nextTick(onClose); - } - }; - - stream.once('error', reject); - stream.on('data', onData); - stream.once('close', onClose); - stream.once('end', onClose); - }); -} diff --git a/packages/discord-voip/src/util/index.ts b/packages/discord-voip/src/util/index.ts index e4ae755459..4ff3f4f2b5 100644 --- a/packages/discord-voip/src/util/index.ts +++ b/packages/discord-voip/src/util/index.ts @@ -3,4 +3,3 @@ export * from './entersState'; export * from './adapter'; -export * from './demuxProbe'; diff --git a/packages/discord-voip/tsconfig.json b/packages/discord-voip/tsconfig.json new file mode 100644 index 0000000000..f1f1679fa3 --- /dev/null +++ b/packages/discord-voip/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@discord-player/tsconfig/base.json", + "include": ["src/**/*"] +} diff --git a/packages/discord-voip/tsup.config.ts b/packages/discord-voip/tsup.config.ts new file mode 100644 index 0000000000..cb32d032ee --- /dev/null +++ b/packages/discord-voip/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from '../../tsup.config'; +import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; + +export default defineConfig({ + esbuildPlugins: [esbuildPluginVersionInjector()], + format: ['cjs'] +}); diff --git a/packages/discord-voip/typedoc.json b/packages/discord-voip/typedoc.json new file mode 100644 index 0000000000..b3eddf3024 --- /dev/null +++ b/packages/discord-voip/typedoc.json @@ -0,0 +1,5 @@ +{ + "entryPoints": ["src/index.ts"], + "excludePrivate": true, + "excludeExternals": true +} diff --git a/packages/equalizer/package.json b/packages/equalizer/package.json index 9f31b48bda..dd7b1b9bd3 100644 --- a/packages/equalizer/package.json +++ b/packages/equalizer/package.json @@ -42,8 +42,8 @@ }, "devDependencies": { "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", + "tsup": "^8.1.0", + "typescript": "^5.5.2", "vitest": "^0.34.6" }, "typedoc": { diff --git a/packages/ffmpeg/package.json b/packages/ffmpeg/package.json index e1c0d9de0c..be08ff26d3 100644 --- a/packages/ffmpeg/package.json +++ b/packages/ffmpeg/package.json @@ -36,8 +36,8 @@ "devDependencies": { "@discord-player/tsconfig": "workspace:^", "@types/node": "^20.3.1", - "tsup": "^7.2.0", - "typescript": "^5.2.2", + "tsup": "^8.1.0", + "typescript": "^5.5.2", "vitest": "^0.34.6" } } diff --git a/packages/node/package.json b/packages/node/package.json index ea599512f6..8fe600a105 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@discord-player/tsconfig": "workspace:^", - "tsup": "^8.1.0" + "tsup": "^8.1.0", + "typescript": "^5.5.2" } } diff --git a/packages/opus/package.json b/packages/opus/package.json index eb8eaef069..e32fa2a3f2 100644 --- a/packages/opus/package.json +++ b/packages/opus/package.json @@ -38,8 +38,8 @@ "devDependencies": { "@discord-player/tsconfig": "workspace:^", "@evan/opus": "^1.0.2", - "tsup": "^7.2.0", - "typescript": "^5.2.2", + "tsup": "^8.1.0", + "typescript": "^5.5.2", "vitest": "^0.34.6" } } diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json index 6fa7319139..b1352453d5 100644 --- a/packages/tsconfig/package.json +++ b/packages/tsconfig/package.json @@ -18,6 +18,7 @@ "url": "https://github.com/Androz2091/discord-player/issues" }, "devDependencies": { - "typescript": "^5.2.2" + "tsup": "^8.1.0", + "typescript": "^5.5.2" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 615cee163a..0bbb045802 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -35,8 +35,8 @@ }, "devDependencies": { "@discord-player/tsconfig": "workspace:^", - "tsup": "^7.2.0", - "typescript": "^5.2.2", + "tsup": "^8.1.0", + "typescript": "^5.5.2", "vitest": "^0.34.6" }, "typedoc": { diff --git a/yarn.lock b/yarn.lock index ca9d7f9355..3fe72d0310 100644 --- a/yarn.lock +++ b/yarn.lock @@ -107,20 +107,20 @@ __metadata: resolution: "@discord-player/equalizer@workspace:packages/equalizer" dependencies: "@discord-player/tsconfig": "workspace:^" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" vitest: "npm:^0.34.6" languageName: unknown linkType: soft -"@discord-player/ffmpeg@npm:^0.1.0, @discord-player/ffmpeg@workspace:^, @discord-player/ffmpeg@workspace:packages/ffmpeg": +"@discord-player/ffmpeg@workspace:^, @discord-player/ffmpeg@workspace:packages/ffmpeg": version: 0.0.0-use.local resolution: "@discord-player/ffmpeg@workspace:packages/ffmpeg" dependencies: "@discord-player/tsconfig": "workspace:^" "@types/node": "npm:^20.3.1" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" vitest: "npm:^0.34.6" languageName: unknown linkType: soft @@ -131,17 +131,18 @@ __metadata: dependencies: "@discord-player/tsconfig": "workspace:^" tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" languageName: unknown linkType: soft -"@discord-player/opus@npm:^0.1.2, @discord-player/opus@workspace:packages/opus": +"@discord-player/opus@workspace:packages/opus": version: 0.0.0-use.local resolution: "@discord-player/opus@workspace:packages/opus" dependencies: "@discord-player/tsconfig": "workspace:^" "@evan/opus": "npm:^1.0.2" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" vitest: "npm:^0.34.6" languageName: unknown linkType: soft @@ -150,7 +151,8 @@ __metadata: version: 0.0.0-use.local resolution: "@discord-player/tsconfig@workspace:packages/tsconfig" dependencies: - typescript: "npm:^5.2.2" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" languageName: unknown linkType: soft @@ -160,32 +162,12 @@ __metadata: dependencies: "@discord-player/tsconfig": "workspace:^" "@discordjs/collection": "npm:^1.1.0" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" vitest: "npm:^0.34.6" languageName: unknown linkType: soft -"@discordjs/builders@npm:^1.1.0": - version: 1.1.0 - resolution: "@discordjs/builders@npm:1.1.0" - dependencies: - "@sapphire/shapeshift": "npm:^3.5.1" - discord-api-types: "npm:^0.36.3" - fast-deep-equal: "npm:^3.1.3" - ts-mixer: "npm:^6.0.1" - tslib: "npm:^2.4.0" - checksum: 10/1f5dba28328544555b0fc7841f261a7b9a69888ae56ec933934c2ad20048cb126f1015c3c4be03925e3fe87877580b96b3cf5ca49578eaba5f777eee2923ed8a - languageName: node - linkType: hard - -"@discordjs/collection@npm:^1.0.1": - version: 1.0.1 - resolution: "@discordjs/collection@npm:1.0.1" - checksum: 10/7245b6d6d159a8fafcb2bb0a384f03c8aa6e247e85e921edebb6330f49869df5ff8b8d3f358b416ad968a601df708b4fb3a5b69c28e024f52ccd006f5f2ea873 - languageName: node - linkType: hard - "@discordjs/collection@npm:^1.1.0": version: 1.3.0 resolution: "@discordjs/collection@npm:1.3.0" @@ -193,21 +175,6 @@ __metadata: languageName: node linkType: hard -"@discordjs/rest@npm:^1.0.1": - version: 1.0.1 - resolution: "@discordjs/rest@npm:1.0.1" - dependencies: - "@discordjs/collection": "npm:^1.0.1" - "@sapphire/async-queue": "npm:^1.3.2" - "@sapphire/snowflake": "npm:^3.2.2" - discord-api-types: "npm:^0.36.3" - file-type: "npm:^17.1.4" - tslib: "npm:^2.4.0" - undici: "npm:^5.8.0" - checksum: 10/fd57cf5abd691cef8b7060f0d6f076eacc96814ad4e73bfa20dadcb23c9b99c3080bced01812157def8b1ef03be5f4aa0d810a2375311ab44a5ee75664890489 - languageName: node - linkType: hard - "@edge-ui/react@npm:0.0.11": version: 0.0.11 resolution: "@edge-ui/react@npm:0.0.11" @@ -3180,13 +3147,6 @@ __metadata: languageName: node linkType: hard -"@sapphire/async-queue@npm:^1.3.2": - version: 1.3.2 - resolution: "@sapphire/async-queue@npm:1.3.2" - checksum: 10/b0d483d8913c5cb1a6c33558ec04a751f8639726ffcf2b76eabb38b0a6ead015c032b6d0400b99946c6121240f50b7997007351f90d62a0ee763cb944a680f52 - languageName: node - linkType: hard - "@sapphire/result@npm:^2.6.0": version: 2.6.0 resolution: "@sapphire/result@npm:2.6.0" @@ -3194,23 +3154,6 @@ __metadata: languageName: node linkType: hard -"@sapphire/shapeshift@npm:^3.5.1": - version: 3.5.1 - resolution: "@sapphire/shapeshift@npm:3.5.1" - dependencies: - fast-deep-equal: "npm:^3.1.3" - lodash.uniqwith: "npm:^4.5.0" - checksum: 10/3791c9b17507eb864656520fcaba17dc660e39f3b2e08d2560438b60ba7752dbdc502ca6db378498f2961d5fe12ca6fae0b4c6e41cff06896d940525981d775b - languageName: node - linkType: hard - -"@sapphire/snowflake@npm:^3.2.2": - version: 3.2.2 - resolution: "@sapphire/snowflake@npm:3.2.2" - checksum: 10/3b88706136e6a1ddcbbdd6b64d161515ef017d0f892bc580d4f9955e09c5edca14eda7594a0dfe8ba78d50273d9a6a02668d586c6b53ceb125f58f91e008a7bb - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -3288,13 +3231,6 @@ __metadata: languageName: node linkType: hard -"@tokenizer/token@npm:^0.3.0": - version: 0.3.0 - resolution: "@tokenizer/token@npm:0.3.0" - checksum: 10/889c1f1e63ac7c92c0ea22d4a2861142f1b43c3d92eb70ec42aa9e9851fab2e9952211d50f541b287781280df2f979bf5600a9c1f91fbc61b7fcf9994e9376a5 - languageName: node - linkType: hard - "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -3407,15 +3343,6 @@ __metadata: languageName: node linkType: hard -"@types/ip@npm:^1.1.0": - version: 1.1.0 - resolution: "@types/ip@npm:1.1.0" - dependencies: - "@types/node": "npm:*" - checksum: 10/2d949112aae75480db13b23282986e516ec5db1c0fcd965f2484c19431f6b0fef55bb7e4372316346b287b1ca9c33aabca6913de3d1e9ac575f60037a7e34bd9 - languageName: node - linkType: hard - "@types/js-yaml@npm:^4.0.0": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" @@ -3607,21 +3534,12 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.3": - version: 8.5.3 - resolution: "@types/ws@npm:8.5.3" - dependencies: - "@types/node": "npm:*" - checksum: 10/08aac698ce6480b532d8311f790a8744ae489ccdd98f374cfe4b8245855439825c64b031abcbba4f30fb280da6cc2b02a4e261e16341d058ffaeecaa24ba2bd3 - languageName: node - linkType: hard - -"@types/ws@npm:^8.5.5": - version: 8.5.5 - resolution: "@types/ws@npm:8.5.5" +"@types/ws@npm:^8.5.10": + version: 8.5.10 + resolution: "@types/ws@npm:8.5.10" dependencies: "@types/node": "npm:*" - checksum: 10/b2d7da5bd469c2ff1ddcfba1da33a556dc02c539e727001e7dc7b4182935154143e96a101cc091686acefb4e115c8ee38111c6634934748b8dd2db0c851c50ab + checksum: 10/9b414dc5e0b6c6f1ea4b1635b3568c58707357f68076df9e7cd33194747b7d1716d5189c0dbdd68c8d2521b148e88184cf881bac7429eb0e5c989b001539ed31 languageName: node linkType: hard @@ -3862,13 +3780,6 @@ __metadata: languageName: node linkType: hard -"@web-scrobbler/metadata-filter@npm:^3.1.0": - version: 3.1.0 - resolution: "@web-scrobbler/metadata-filter@npm:3.1.0" - checksum: 10/71e0ff278efc0c6d09bc60cf1cde67e67369d6fb13aee7dcb946a18f02c5387bfb8a391e65fe945f7a4c4cd3fdaad02539a197ff5e14961719eb1349fc42d393 - languageName: node - linkType: hard - "abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -5226,24 +5137,10 @@ __metadata: languageName: node linkType: hard -"discord-api-types@npm:^0.36.3": - version: 0.36.3 - resolution: "discord-api-types@npm:0.36.3" - checksum: 10/ada32806cb87e32a142a32a2ffd9256fb104d5570e74abe7be413c1d6d23dd51087b61447f1887e63ee328bdc71f35023d3bc9558b19242e9f2cf4da40b51f7b - languageName: node - linkType: hard - -"discord-api-types@npm:^0.37.0": - version: 0.37.0 - resolution: "discord-api-types@npm:0.37.0" - checksum: 10/4abb8289d959a98caddc0adcf9983d3f409fc05fe6f96412f8ac0df4e32cbdd248b299214d72271093fb6f19493267e6b050895f57081191ed1aeec36f5f754e - languageName: node - linkType: hard - -"discord-api-types@npm:^0.37.50": - version: 0.37.52 - resolution: "discord-api-types@npm:0.37.52" - checksum: 10/c9212437192cbd782441bc1da7c8caf4114ada6241fd9673eda70df6f8b3003acf3fbe9c59d5be22cf9bb941ac5c3f234895be79f1476f9a1ce0096e2d52367e +"discord-api-types@npm:^0.37.91": + version: 0.37.91 + resolution: "discord-api-types@npm:0.37.91" + checksum: 10/046c2f42161e65bdd26d68869c4ceae4aa0937d6535f5ba62ab0e8ea0bcacbfa93f35d9b8cbccd5929ab0fb094e5f82d2600cc931637295030ebca5cd4d9647e languageName: node linkType: hard @@ -5256,67 +5153,25 @@ __metadata: "@discord-player/node": "workspace:^" "@discord-player/tsconfig": "workspace:^" "@discord-player/utils": "workspace:^" - "@types/ip": "npm:^1.1.0" "@types/node": "npm:^18.6.3" - "@types/ws": "npm:^8.5.3" - "@web-scrobbler/metadata-filter": "npm:^3.1.0" - discord-api-types: "npm:^0.37.0" - discord-voip: "npm:^0.1.3" - discord.js: "npm:^14.1.2" - ip: "npm:^2.0.1" - libsodium-wrappers: "npm:^0.7.13" - opusscript: "npm:^0.0.8" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" + discord-api-types: "npm:^0.37.91" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" vitest: "npm:^0.34.6" - ws: "npm:^8.17.0" languageName: unknown linkType: soft -"discord-voip@npm:^0.1.3": - version: 0.1.3 - resolution: "discord-voip@npm:0.1.3" - dependencies: - "@discord-player/ffmpeg": "npm:^0.1.0" - "@discord-player/opus": "npm:^0.1.2" - "@types/ws": "npm:^8.5.5" - discord-api-types: "npm:^0.37.50" - prism-media: "npm:^1.3.5" - tslib: "npm:^2.6.1" - ws: "npm:^8.13.0" - checksum: 10/640db80b7ceb5be392b30d3f801fa9eaa2207204eaaa04fd265404f26d17fad4d7976de9c3d14a6c525b8bf23a3b69ac307300230e01bdd5f4689e3740961426 - languageName: node - linkType: hard - "discord-voip@workspace:packages/discord-voip": version: 0.0.0-use.local resolution: "discord-voip@workspace:packages/discord-voip" dependencies: "@discord-player/tsconfig": "workspace:^" - tsup: "npm:^7.2.0" - typescript: "npm:^5.2.2" + "@types/ws": "npm:^8.5.10" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" languageName: unknown linkType: soft -"discord.js@npm:^14.1.2": - version: 14.1.2 - resolution: "discord.js@npm:14.1.2" - dependencies: - "@discordjs/builders": "npm:^1.1.0" - "@discordjs/collection": "npm:^1.0.1" - "@discordjs/rest": "npm:^1.0.1" - "@sapphire/snowflake": "npm:^3.2.2" - "@types/ws": "npm:^8.5.3" - discord-api-types: "npm:^0.36.3" - fast-deep-equal: "npm:^3.1.3" - lodash.snakecase: "npm:^4.1.1" - tslib: "npm:^2.4.0" - undici: "npm:^5.8.0" - ws: "npm:^8.8.1" - checksum: 10/b9155d65eb17708f0765f96848cedcd14a096d2bee0b657caa05a7f20f549491e1d894ac155e421f06a541ef6832bb742b1edd79661ad9a4e4baa5302f3cc79e - languageName: node - linkType: hard - "dlv@npm:^1.1.3": version: 1.1.3 resolution: "dlv@npm:1.1.3" @@ -5347,6 +5202,8 @@ __metadata: resolution: "docs@workspace:apps/docs" dependencies: mintlify: "npm:^4.0.160" + tsup: "npm:^8.1.0" + typescript: "npm:^5.5.2" languageName: unknown linkType: soft @@ -6479,17 +6336,6 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^17.1.4": - version: 17.1.6 - resolution: "file-type@npm:17.1.6" - dependencies: - readable-web-to-node-stream: "npm:^3.0.2" - strtok3: "npm:^7.0.0-alpha.9" - token-types: "npm:^5.0.0-alpha.2" - checksum: 10/47c69b4046e31142492a135982b9a9e4873b368919a2e66d0ebdc04143b6d2e1225b4bec820668c442ef201b54d03569df08b6052edc6015b1022c236784e1c1 - languageName: node - linkType: hard - "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -7612,13 +7458,6 @@ __metadata: languageName: node linkType: hard -"ip@npm:^2.0.1": - version: 2.0.1 - resolution: "ip@npm:2.0.1" - checksum: 10/d6dd154e1bc5e8725adfdd6fb92218635b9cbe6d873d051bd63b178f009777f751a5eea4c67021723a7056325fc3052f8b6599af0a2d56f042c93e684b4a0349 - languageName: node - linkType: hard - "ipaddr.js@npm:1.9.1": version: 1.9.1 resolution: "ipaddr.js@npm:1.9.1" @@ -8178,22 +8017,6 @@ __metadata: languageName: node linkType: hard -"libsodium-wrappers@npm:^0.7.13": - version: 0.7.13 - resolution: "libsodium-wrappers@npm:0.7.13" - dependencies: - libsodium: "npm:^0.7.13" - checksum: 10/e5f7f5baf37095a764e3f5037ab47d65343db0c0efca4add80a5c2037ac2cb9fb05f00646f7b75220bd4f30bccc02e9742603ceb8eb97e5c305103cd4f86392d - languageName: node - linkType: hard - -"libsodium@npm:^0.7.13": - version: 0.7.13 - resolution: "libsodium@npm:0.7.13" - checksum: 10/0105523135c1ed1c4ee853a36cd3b3b562ed150f5888ae61be4b2945f0b1010e7ecaad4cdc9333beac034a88bdf470a93f22609877a06f15f5d690a9591e0d7c - languageName: node - linkType: hard - "lilconfig@npm:^2.0.5": version: 2.0.6 resolution: "lilconfig@npm:2.0.6" @@ -8245,13 +8068,6 @@ __metadata: languageName: node linkType: hard -"lodash.snakecase@npm:^4.1.1": - version: 4.1.1 - resolution: "lodash.snakecase@npm:4.1.1" - checksum: 10/82ed40935d840477ef8fee64f9f263f75989c6cde36b84aae817246d95826228e1b5a7f6093c51de324084f86433634c7af244cb89496633cacfe443071450d0 - languageName: node - linkType: hard - "lodash.sortby@npm:^4.7.0": version: 4.7.0 resolution: "lodash.sortby@npm:4.7.0" @@ -8259,13 +8075,6 @@ __metadata: languageName: node linkType: hard -"lodash.uniqwith@npm:^4.5.0": - version: 4.5.0 - resolution: "lodash.uniqwith@npm:4.5.0" - checksum: 10/b71e2320d7ef2e8d95b5b042a757417e197710cd3fd6c70e2ec3a9583b03496487d915158bc34928cfc56a9d8c3cad6c78d6d7ce6a8bef9218afafac6a963ed6 - languageName: node - linkType: hard - "lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -9845,13 +9654,6 @@ __metadata: languageName: node linkType: hard -"opusscript@npm:^0.0.8": - version: 0.0.8 - resolution: "opusscript@npm:0.0.8" - checksum: 10/613b2625a1703bc5a706861392c27aedbe41cb3c224dee3b2c3b2bfb93e0801fbaac3244cdeeea8892ad6e6d26ee1aeb09a9418456becc1a3af6d657f35a166f - languageName: node - linkType: hard - "ora@npm:^6.1.2": version: 6.3.1 resolution: "ora@npm:6.3.1" @@ -10068,13 +9870,6 @@ __metadata: languageName: node linkType: hard -"peek-readable@npm:^5.0.0": - version: 5.0.0 - resolution: "peek-readable@npm:5.0.0" - checksum: 10/d342f02dd0c8a6b4bd0e7519a93d545b2b19375200e79a7431f0f1ec3f91e22b2217fa3a15cde95f6ab388ce6fce8aae75794d84b9b39c5836eb7c5f55e7ee9e - languageName: node - linkType: hard - "periscopic@npm:^3.0.0": version: 3.1.0 resolution: "periscopic@npm:3.1.0" @@ -10255,27 +10050,6 @@ __metadata: languageName: node linkType: hard -"prism-media@npm:^1.3.5": - version: 1.3.5 - resolution: "prism-media@npm:1.3.5" - peerDependencies: - "@discordjs/opus": ">=0.8.0 <1.0.0" - ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0 - node-opus: ^0.3.3 - opusscript: ^0.0.8 - peerDependenciesMeta: - "@discordjs/opus": - optional: true - ffmpeg-static: - optional: true - node-opus: - optional: true - opusscript: - optional: true - checksum: 10/a4e7e3cbb0b818e58926091e4c41a7032b16ff13beed052036e06915db8ed8bf6de67aabb98c2bcf7729df68eee84ae7f9970ed1efb49f805f6c5a02884e563d - languageName: node - linkType: hard - "prism-react-renderer@npm:^2.0.3": version: 2.0.5 resolution: "prism-react-renderer@npm:2.0.5" @@ -10565,15 +10339,6 @@ __metadata: languageName: node linkType: hard -"readable-web-to-node-stream@npm:^3.0.2": - version: 3.0.2 - resolution: "readable-web-to-node-stream@npm:3.0.2" - dependencies: - readable-stream: "npm:^3.6.0" - checksum: 10/d3a5bf9d707c01183d546a64864aa63df4d9cb835dfd2bf89ac8305e17389feef2170c4c14415a10d38f9b9bfddf829a57aaef7c53c8b40f11d499844bf8f1a4 - languageName: node - linkType: hard - "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -11703,16 +11468,6 @@ __metadata: languageName: node linkType: hard -"strtok3@npm:^7.0.0-alpha.9": - version: 7.0.0 - resolution: "strtok3@npm:7.0.0" - dependencies: - "@tokenizer/token": "npm:^0.3.0" - peek-readable: "npm:^5.0.0" - checksum: 10/4f2269679fcfce1e9fe0600eff361ea4c687ae0a0e8d9dab6703811071cd92545cbcb32d4ace3d3aa591f777cec1a3e8aeecd5efd71ae216fd2962a7a238b4ab - languageName: node - linkType: hard - "style-to-object@npm:^0.4.1": version: 0.4.1 resolution: "style-to-object@npm:0.4.1" @@ -11960,16 +11715,6 @@ __metadata: languageName: node linkType: hard -"token-types@npm:^5.0.0-alpha.2": - version: 5.0.0-alpha.2 - resolution: "token-types@npm:5.0.0-alpha.2" - dependencies: - "@tokenizer/token": "npm:^0.3.0" - ieee754: "npm:^1.2.1" - checksum: 10/301c1c45bcf121824aa34cef98b539edaaeb93cbd4b499fa679f4044763dbe1b6f1a648bfade3bc2b0eb82440148f763f061e6e8b1ee226fccaa9e8bc847554d - languageName: node - linkType: hard - "tr46@npm:^1.0.1": version: 1.0.1 resolution: "tr46@npm:1.0.1" @@ -12025,13 +11770,6 @@ __metadata: languageName: node linkType: hard -"ts-mixer@npm:^6.0.1": - version: 6.0.1 - resolution: "ts-mixer@npm:6.0.1" - checksum: 10/3867b87fe9c0345476c49499a0a6ba867c6a33289b895685f99633cfeda49ceba331985f1bea6ae721ebb62484b7f8bf483dd5f4cc8bb20d5edd5e58565580bf - languageName: node - linkType: hard - "tsconfig-paths@npm:^3.14.1": version: 3.14.2 resolution: "tsconfig-paths@npm:3.14.2" @@ -12072,13 +11810,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.6.1": - version: 2.6.1 - resolution: "tslib@npm:2.6.1" - checksum: 10/5cf1aa7ea4ca7ee9b8aa3d80eb7ee86634b307fbefcb948a831c2b13728e21e156ef7fb9edcbe21f05c08f65e4cf4480587086f31133491ba1a49c9e0b28fc75 - languageName: node - linkType: hard - "tsup@npm:^7.2.0": version: 7.2.0 resolution: "tsup@npm:7.2.0" @@ -12326,6 +12057,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.5.2": + version: 5.5.2 + resolution: "typescript@npm:5.5.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/9118b20f248e76b0dbff8737fef65dfa89d02668d4e633d2c5ceac99033a0ca5e8a1c1a53bc94da68e8f67677a88f318663dde859c9e9a09c1e116415daec2ba + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A5.1.3#optional!builtin": version: 5.1.3 resolution: "typescript@patch:typescript@npm%3A5.1.3#optional!builtin::version=5.1.3&hash=5da071" @@ -12356,6 +12097,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.5.2#optional!builtin": + version: 5.5.2 + resolution: "typescript@patch:typescript@npm%3A5.5.2#optional!builtin::version=5.5.2&hash=b45daf" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/28b3de2ddaf63a7620e7ddbe5d377af71ce93ecc558c41bf0e3d88661d8e6e7aa6c7739164fef98055f69819e41faca49252938ef3633a3dff2734cca6a9042e + languageName: node + linkType: hard + "ufo@npm:^1.3.0": version: 1.3.1 resolution: "ufo@npm:1.3.1" @@ -12382,13 +12133,6 @@ __metadata: languageName: node linkType: hard -"undici@npm:^5.8.0": - version: 5.8.0 - resolution: "undici@npm:5.8.0" - checksum: 10/d8c7f13085e5a18aa0a5201f75e0e3534acafe0f4f3400b0440a428ae17bb77a4da643dcd3049a80f9738f9f3927886b05afccc668ddc8af7463df4174f2847f - languageName: node - linkType: hard - "unherit@npm:^3.0.0": version: 3.0.1 resolution: "unherit@npm:3.0.1" @@ -13134,51 +12878,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0": - version: 8.13.0 - resolution: "ws@npm:8.13.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/1769532b6fdab9ff659f0b17810e7501831d34ecca23fd179ee64091dd93a51f42c59f6c7bb4c7a384b6c229aca8076fb312aa35626257c18081511ef62a161d - languageName: node - linkType: hard - -"ws@npm:^8.17.0": - version: 8.17.0 - resolution: "ws@npm:8.17.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/5e1dcb0ae70c6e2f158f5b446e0a72a2cd335b07aba73ee1872e9bae1285382286a10e53ed479db21bdd690a5dfd05641a768611ebb236253c62fefa43ef58b4 - languageName: node - linkType: hard - -"ws@npm:^8.8.1": - version: 8.8.1 - resolution: "ws@npm:8.8.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/65e33447e8ff83f76a3e0cefc28be02e73db37b10c7bf282121ec97ad972407f5b960609c8868ca6f48692d118a9d93ea0c434a4671e15bee1a13e840caa1adf - languageName: node - linkType: hard - "ws@npm:~8.11.0": version: 8.11.0 resolution: "ws@npm:8.11.0" From 4d2a7c04214eb13be49de281f68576dc0cac3db4 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 30 Jun 2024 18:33:23 +0545 Subject: [PATCH 6/7] feat: add connector base --- packages/node/src/PlayerNode.ts | 44 ++++++++++++++++++++++-- packages/node/src/connector/Connector.ts | 37 ++++++++++++++++++++ packages/node/src/connector/IPC.ts | 19 ++++++++++ packages/node/src/connector/TCP.ts | 19 ++++++++++ 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 packages/node/src/connector/Connector.ts create mode 100644 packages/node/src/connector/IPC.ts create mode 100644 packages/node/src/connector/TCP.ts diff --git a/packages/node/src/PlayerNode.ts b/packages/node/src/PlayerNode.ts index 06421d3386..d9019c7121 100644 --- a/packages/node/src/PlayerNode.ts +++ b/packages/node/src/PlayerNode.ts @@ -1,4 +1,7 @@ import { randomUUID } from 'node:crypto'; +import type { Connector } from './connector/Connector'; +import { IPC } from './connector/IPC'; +import { TCP } from './connector/TCP'; /** * Represents a player node object. @@ -24,6 +27,10 @@ export interface PlayerNodeOptions { * The password of this node. */ password: string; + /** + * Whether this node is an IPC node. + */ + ipc: boolean; } /** @@ -50,6 +57,11 @@ export interface PlayerNodeInit { */ export const DISCORD_PLAYER_PROTOCOL = 'discord-player:'; +/** + * The hostname for discord player IPC nodes. + */ +export const DISCORD_PLAYER_IPC_HOSTNAME = 'ipc.discord-player.js.org'; + function validateNodeOptions(node: PlayerNodeOptions): PlayerNodeOptions { const required = ['host', 'clientId', 'password'] as const; @@ -84,6 +96,16 @@ export class PlayerNode { */ private readonly node: PlayerNodeOptions; + /** + * Whether this player node uses IPC. + */ + #isIPC = false; + + /** + * The connector for this player node. + */ + private readonly connector: Connector; + /** * Creates a new player node. * @param node - The node config or node string to use @@ -95,17 +117,31 @@ export class PlayerNode { } else { this.node = validateNodeOptions(node); } + + this.#isIPC = this.node.ipc; + this.connector = this.#isIPC ? new IPC(this) : new TCP(this); + } + + /** + * Whether this player node is an IPC node. + */ + public get isIPCNode() { + return this.#isIPC; } /** * Connects to this player node. */ - public async connect() {} + public async connect() { + await this.connector.connect(); + } /** * Destroy this player node. */ - public async delete() {} + public async delete() { + await this.connector.disconnect(); + } /** * Sends a packet to the player node. @@ -166,13 +202,15 @@ export class PlayerNode { } const isSecure = searchParams.get('secure') === 'true'; + const isIpc = hostname === DISCORD_PLAYER_IPC_HOSTNAME; return { host: hostname, port: port ? Number(port) : null, secure: isSecure, clientId: username, - password + password, + ipc: isIpc }; } } diff --git a/packages/node/src/connector/Connector.ts b/packages/node/src/connector/Connector.ts new file mode 100644 index 0000000000..681f5a9d89 --- /dev/null +++ b/packages/node/src/connector/Connector.ts @@ -0,0 +1,37 @@ +import { EventEmitter } from '@discord-player/utils'; +import type { PlayerNode } from '../PlayerNode'; + +export interface ConnectorEvents { + ready: () => Awaited; + close: () => Awaited; + error: (error: Error) => Awaited; +} + +export abstract class Connector extends EventEmitter { + /** + * Creates a new connector. + * @param node - The player node instance + */ + public constructor(public readonly node: PlayerNode) { + super(); + } + + /** + * Connects to the node. + */ + public abstract connect(): Promise; + /** + * Disconnects from the node. + */ + public abstract disconnect(): Promise; + /** + * Whether the node is connected. + */ + public abstract isConnected(): boolean; + /** + * Send a packet to the node. + * @param data - The data to send + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public abstract send(data: any): Promise; +} diff --git a/packages/node/src/connector/IPC.ts b/packages/node/src/connector/IPC.ts new file mode 100644 index 0000000000..2b73e96a00 --- /dev/null +++ b/packages/node/src/connector/IPC.ts @@ -0,0 +1,19 @@ +import { Connector } from './Connector'; + +export class IPC extends Connector { + public connect(): Promise { + throw new Error('Method not implemented.'); + } + + public disconnect(): Promise { + throw new Error('Method not implemented.'); + } + + public isConnected(): boolean { + throw new Error('Method not implemented.'); + } + + public send(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/node/src/connector/TCP.ts b/packages/node/src/connector/TCP.ts new file mode 100644 index 0000000000..0c04ad5cfd --- /dev/null +++ b/packages/node/src/connector/TCP.ts @@ -0,0 +1,19 @@ +import { Connector } from './Connector'; + +export class TCP extends Connector { + public connect(): Promise { + throw new Error('Method not implemented.'); + } + + public disconnect(): Promise { + throw new Error('Method not implemented.'); + } + + public isConnected(): boolean { + throw new Error('Method not implemented.'); + } + + public send(): Promise { + throw new Error('Method not implemented.'); + } +} From 7831a24a957332dfd4764f411bbd6fae24081170 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:15:00 +0545 Subject: [PATCH 7/7] feat: init core --- packages/core/LICENSE | 21 ++++++ packages/core/README.md | 15 +++++ packages/core/__test__/sum.spec.ts | 8 +++ packages/core/package.json | 45 +++++++++++++ packages/core/src/common/constants.ts | 7 ++ packages/core/src/index.ts | 6 ++ packages/core/src/server.ts | 0 packages/core/src/worker/entrypoint.ts | 49 ++++++++++++++ packages/core/src/workers.ts | 88 ++++++++++++++++++++++++++ packages/core/tsconfig.json | 6 ++ packages/core/tsup.config.ts | 7 ++ packages/core/vitest.config.ts | 9 +++ packages/discord-voip/package.json | 5 ++ yarn.lock | 17 ++++- 14 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 packages/core/LICENSE create mode 100644 packages/core/README.md create mode 100644 packages/core/__test__/sum.spec.ts create mode 100644 packages/core/package.json create mode 100644 packages/core/src/common/constants.ts create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/server.ts create mode 100644 packages/core/src/worker/entrypoint.ts create mode 100644 packages/core/src/workers.ts create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/tsup.config.ts create mode 100644 packages/core/vitest.config.ts diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 0000000000..fe07fc7364 --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Androz2091 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000000..d7cc4767e4 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,15 @@ +# `@discord-player/core` + +Discord Player `@discord-player/core` library + +## Installation + +```sh +$ yarn add @discord-player/core +``` + +## Example + +```js +import pkg from "@discord-player/core" +``` \ No newline at end of file diff --git a/packages/core/__test__/sum.spec.ts b/packages/core/__test__/sum.spec.ts new file mode 100644 index 0000000000..6d3cd1af62 --- /dev/null +++ b/packages/core/__test__/sum.spec.ts @@ -0,0 +1,8 @@ +import { add } from '../src'; +import { describe, it, expect } from 'vitest'; + +describe('Sum', () => { + it('should add two numbers', () => { + expect(add(2, 2)).toBe(4); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000000..600587dc97 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,45 @@ +{ + "name": "@discord-player/core", + "version": "0.1.0", + "description": "A complete framework to simplify the implementation of music commands for Discord bots", + "keywords": [ + "discord-player", + "music", + "bot", + "discord.js", + "javascript", + "voip", + "lavalink", + "lavaplayer" + ], + "author": "Androz2091 ", + "homepage": "https://discord-player.js.org", + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Androz2091/discord-player.git" + }, + "scripts": { + "build": "tsup", + "build:check": "tsc --noEmit", + "lint": "eslint src --ext .ts --fix", + "test": "vitest", + "coverage": "vitest run --coverage" + }, + "bugs": { + "url": "https://github.com/Androz2091/discord-player/issues" + }, + "devDependencies": { + "@discord-player/tsconfig": "workspace:^" + }, + "dependencies": { + "@discord-player/utils": "workspace:^", + "discord-voip": "workspace:^" + } +} diff --git a/packages/core/src/common/constants.ts b/packages/core/src/common/constants.ts new file mode 100644 index 0000000000..3e484d881c --- /dev/null +++ b/packages/core/src/common/constants.ts @@ -0,0 +1,7 @@ +export enum WorkerOp { + OP_PING +} + +export enum WorkerAckOp { + OP_ACK_PING +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000000..ac4ac60b7c --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,6 @@ +export const add = (a: number, b: number) => { + return a + b; +}; + +// eslint-disable-next-line @typescript-eslint/no-inferrable-types +export const version: string = '[VI]{{inject}}[/VI]'; diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/src/worker/entrypoint.ts b/packages/core/src/worker/entrypoint.ts new file mode 100644 index 0000000000..2d9a34eda9 --- /dev/null +++ b/packages/core/src/worker/entrypoint.ts @@ -0,0 +1,49 @@ +import { isMainThread, parentPort } from 'node:worker_threads'; +import { WorkerAckOp, WorkerOp } from '../common/constants'; + +interface WorkerMessage { + op: WorkerOp; + d: T; +} + +interface WorkerMessageAck { + op: WorkerAckOp; + d: T; +} + +class WorkerListener { + public constructor() { + if (!parentPort) throw new Error('This script must be run as a worker'); + + parentPort.on('message', async (message) => { + try { + this.validate(message); + await this.handleMessage(message); + } catch { + // + } + }); + } + + private validate(message: WorkerMessage) { + if (!('op' in message) || !('d' in message)) throw new Error('Invalid message format'); + if (!(message.op in WorkerOp)) throw new Error('Invalid operation code'); + + return true; + } + + private send(message: WorkerMessageAck) { + parentPort!.postMessage(message); + } + + private async handleMessage(message: WorkerMessage) { + switch (message.op) { + case WorkerOp.OP_PING: + return this.send({ op: WorkerAckOp.OP_ACK_PING, d: null }); + } + } +} + +if (!isMainThread) { + new WorkerListener(); +} diff --git a/packages/core/src/workers.ts b/packages/core/src/workers.ts new file mode 100644 index 0000000000..41fb45dcc6 --- /dev/null +++ b/packages/core/src/workers.ts @@ -0,0 +1,88 @@ +import { Worker } from 'node:worker_threads'; +import { Collection, EventEmitter } from '@discord-player/utils'; + +interface IWorkerStats { + memoryUsed: number; + subscriptions: number; +} + +interface WorkerInfo { + worker: Worker; + lastAccess: number; + estimatedClients: number; + stats: IWorkerStats; +} + +export enum WorkerDistributionMode { + Balanced = 'balanced', + LeastLoad = 'least-load', + Random = 'random' +} + +export interface WorkerConfig { + maxWorkers: number; + distributionMode: WorkerDistributionMode; +} + +export interface WorkerEvents { + error: (error: Error) => void; + exit: () => void; + ready: () => void; +} + +export class WorkerManager extends EventEmitter { + private workers = new Collection(); + + public constructor(public readonly config: WorkerConfig) { + super(); + } + + public getOptimalWorker(): WorkerInfo | undefined { + if (this.workers.size === 0) return undefined; + + switch (this.config.distributionMode) { + case WorkerDistributionMode.LeastLoad: + return this.workers.sort((a, b) => a.stats.memoryUsed - b.stats.memoryUsed).first(); + case WorkerDistributionMode.Balanced: + return this.workers.sort((a, b) => a.estimatedClients - b.estimatedClients).first(); + case WorkerDistributionMode.Random: + return this.workers.random(); + } + } + + public canCreateWorker(): boolean { + return this.workers.size < this.config.maxWorkers; + } + + public create() { + if (!this.canCreateWorker()) return this.getOptimalWorker()!; + + const thread = new Worker('./worker/entrypoint.js'); + + thread.on('online', () => { + this.emit('ready'); + }); + + thread.on('error', (error) => { + void error; + }); + + thread.once('exit', () => { + this.workers.delete(thread.threadId); + }); + + const workerInfo: WorkerInfo = { + worker: thread, + estimatedClients: 0, + stats: { + memoryUsed: 0, + subscriptions: 0 + }, + lastAccess: Date.now() + }; + + this.workers.set(thread.threadId, workerInfo); + + return workerInfo; + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000000..3c774c914c --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@discord-player/tsconfig/base.json", + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts new file mode 100644 index 0000000000..bc934bc0a7 --- /dev/null +++ b/packages/core/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from '../../tsup.config'; +import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector'; + +export default defineConfig({ + esbuildPlugins: [esbuildPluginVersionInjector()], + entry: ['./src'] +}); diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts new file mode 100644 index 0000000000..e055dbaa74 --- /dev/null +++ b/packages/core/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + dir: `${__dirname}/__test__`, + passWithNoTests: true, + watch: false + } +}); diff --git a/packages/discord-voip/package.json b/packages/discord-voip/package.json index f2540419b9..afb27682c2 100644 --- a/packages/discord-voip/package.json +++ b/packages/discord-voip/package.json @@ -37,5 +37,10 @@ "@discord-player/tsconfig": "workspace:^", "tsup": "^7.2.0", "typescript": "^5.2.2" + }, + "dependencies": { + "@discord-player/ffmpeg": "workspace:^", + "@discord-player/opus": "workspace:^", + "@discord-player/utils": "workspace:^" } } diff --git a/yarn.lock b/yarn.lock index ca9d7f9355..a9f8b0ce14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,6 +82,16 @@ __metadata: languageName: node linkType: hard +"@discord-player/core@workspace:packages/core": + version: 0.0.0-use.local + resolution: "@discord-player/core@workspace:packages/core" + dependencies: + "@discord-player/tsconfig": "workspace:^" + "@discord-player/utils": "workspace:^" + discord-voip: "workspace:^" + languageName: unknown + linkType: soft + "@discord-player/discord-player@workspace:.": version: 0.0.0-use.local resolution: "@discord-player/discord-player@workspace:." @@ -134,7 +144,7 @@ __metadata: languageName: unknown linkType: soft -"@discord-player/opus@npm:^0.1.2, @discord-player/opus@workspace:packages/opus": +"@discord-player/opus@npm:^0.1.2, @discord-player/opus@workspace:^, @discord-player/opus@workspace:packages/opus": version: 0.0.0-use.local resolution: "@discord-player/opus@workspace:packages/opus" dependencies: @@ -5288,11 +5298,14 @@ __metadata: languageName: node linkType: hard -"discord-voip@workspace:packages/discord-voip": +"discord-voip@workspace:^, discord-voip@workspace:packages/discord-voip": version: 0.0.0-use.local resolution: "discord-voip@workspace:packages/discord-voip" dependencies: + "@discord-player/ffmpeg": "workspace:^" + "@discord-player/opus": "workspace:^" "@discord-player/tsconfig": "workspace:^" + "@discord-player/utils": "workspace:^" tsup: "npm:^7.2.0" typescript: "npm:^5.2.2" languageName: unknown