├── .gitignore ├── .idea ├── Kazagumo.iml ├── discord.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── .prettierrc ├── LICENSE.md ├── README.md ├── SECURITY.md ├── appveyor.yml ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── Kazagumo.Kazagumo.html │ ├── Managers_KazagumoPlayer.KazagumoPlayer.html │ ├── Managers_Supports_KazagumoQueue.KazagumoQueue.html │ ├── Managers_Supports_KazagumoTrack.KazagumoTrack.html │ ├── Modules_Interfaces.KazagumoError.html │ ├── Modules_Interfaces.KazagumoPlugin.html │ ├── Modules_Utils.KazagumoUtils.html │ └── Plugins_PlayerMoved.KazagumoPlugin.html ├── enums │ ├── Modules_Interfaces.PlayerState.html │ ├── Modules_Interfaces.State.html │ └── Modules_Interfaces.VoiceState.html ├── functions │ └── Modules_Interfaces.escapeRegExp.html ├── index.html ├── interfaces │ ├── Kazagumo.KazagumoEvents.html │ ├── Modules_Interfaces.CreatePlayerOptions.html │ ├── Modules_Interfaces.KazagumoOptions.html │ ├── Modules_Interfaces.KazagumoPlayerOptions.html │ ├── Modules_Interfaces.KazagumoSearchOptions.html │ ├── Modules_Interfaces.KazagumoSearchResult.html │ ├── Modules_Interfaces.Payload.html │ ├── Modules_Interfaces.PlayOptions.html │ ├── Modules_Interfaces.PlayerMovedChannels.html │ ├── Modules_Interfaces.RawTrack.html │ └── Modules_Interfaces.ResolveOptions.html ├── modules │ ├── Index.html │ ├── Kazagumo.html │ ├── Managers_KazagumoPlayer.html │ ├── Managers_Supports_KazagumoQueue.html │ ├── Managers_Supports_KazagumoTrack.html │ ├── Modules_Interfaces.html │ ├── Modules_Plugins.html │ ├── Modules_Utils.html │ └── Plugins_PlayerMoved.html ├── types │ ├── Modules_Interfaces.PlayerMovedState.html │ ├── Modules_Interfaces.SearchEngines.html │ ├── Modules_Interfaces.SearchResultTypes.html │ ├── Modules_Interfaces.YoutubeThumbnail.html │ └── Modules_Utils.Constructor.html └── variables │ ├── Index.version.html │ ├── Modules_Interfaces.Events.html │ ├── Modules_Interfaces.SourceIDs.html │ ├── Modules_Interfaces.SupportedSources.html │ └── Modules_Plugins.default.html ├── package-lock.json ├── package.json ├── src ├── Index.ts ├── Kazagumo.ts ├── Managers │ ├── KazagumoPlayer.ts │ └── Supports │ │ ├── KazagumoQueue.ts │ │ └── KazagumoTrack.ts ├── Modules │ ├── Interfaces.ts │ ├── Plugins.ts │ └── Utils.ts └── Plugins │ └── PlayerMoved.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test 4 | .idea 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port -------------------------------------------------------------------------------- /.idea/Kazagumo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 Takiyo Takahashi 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kazagumo 2 | #### A [Shoukaku](https://github.com/Deivu/Shoukaku) wrapper with built in queue system 3 | 4 |        5 | 6 |  7 | > Kazagumo © Azur Lane 8 | 9 | ## Features: 10 | 11 | ✓ Built-in queue system 12 | ✓ Easy to use 13 | ✓ Plugin system 14 | ✓ Uses shoukaku v4 + capable of Lavalink v4 15 | ✓ Stable _🙏_ 16 | 17 | ## Note 18 | ⚠️Please check [Environment](#environment) that Kazagumo 3.2.0 is verified working on. It's recommended to use the latest version of lavalink. If you encounter any problem, try using previous [version](https://www.npmjs.com/package/kazagumo/v/3.1.2). If issue still persist, please [open an issue](https://github.com/Takiyo0/Kazagumo/issues) or ask me in [Discord](https://discord.gg/nPPW2Gzqg2) (I will answer if I have time) ⚠️ 19 | 20 | ## Documentation 21 | Please read the docs first before asking methods 22 | > Kazagumo; https://takiyo0.github.io/Kazagumo 23 | > [Shoukaku](https://github.com/Deivu/Shoukaku) by [Deivu](https://github.com/Deivu); https://deivu.github.io/Shoukaku 24 | 25 | ## Installation 26 | 27 | > npm i kazagumo 28 | 29 | ## Metadata 30 | 31 | > version: 3.3.0 32 | > pre-release: false 33 | > Last build: 6-1-2025 22.13 PM 34 | 35 | ## Environment 36 | The new lavalink system that separate YouTube plugins made configuration a bit harder. I will list all working environment that's known working. 37 | 38 | | Environment | Case 1 | Case 2 | Case 3 | 39 | |----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| 40 | | Lavalink Version | v4.0.7 | v4.0.7 | v4.0.7 | 41 | | Youtube Plugin Version | v1.7.2 | v1.7.2 | none | 42 | | [LavaSrc](https://google.com) Plugin Version | v4.1.1 | v4.1.1 | v4.1.1 | 43 | | Kazagumo Version | v3.2.0 | v3.2.0 | v3.2.0 | 44 | | Shoukaku Version | v4.1.0 (built-in v3.2.0) | v4.1.0 (built-in v3.2.0) | v4.1.0 (built-in v3.2.0) | 45 | | Youtube Plugin Config | youtube.oauth.enabled = trueyoutube.oauth.accessToken = "filled"youtube.oauth.clients = MUSIC,ANDROID_TESTSUITE,WEB | youtube.oauth.enabled = trueyoutube.oauth.accessToken = "filled"youtube.oauth.clients = MUSIC,ANDROID_TESTSUITE,WEB,TVHTML5EMBEDDED | none | 46 | | Lavalink Config | server.sources.youtube = falseserver.sources.youtubeSearchEnabled = false | server.sources.youtube = falseserver.sources.youtubeSearchEnabled = false | server.sources.youtube = trueserver.sources.youtubeSearchEnabled = true | 47 | | LavaSrc Config | lavasrc.sources.youtube = true | lavasrc.sources.youtube = true | lavasrc.sources.youtube = true | 48 | | **Result** | | | | 49 | | YouTube Playlist Load* | ✅ | ❌ | ✅ | 50 | | YouTube Track Load | ✅ | ✅ | ❌ | 51 | | YouTube Search | ✅ | ✅ | ✅ | 52 | | LavaSrc Spotify Playlist Load | ✅ | ✅ | ✅ | 53 | | LavaSrc Spotify Track Load | ✅ | ✅ | ✅ | 54 | | LavaSrc Spotify Search (spsearch:query)** | ✅ | ✅ | ✅ | 55 | | **Summary** | ✅ works just fine | ➖ cannot load youtube playlist | ❌ cannot play any track youtube related. including spotify | 56 | 57 | Note: 58 | - `*` = youtube playlist load with YouTube plugin requires oauth enabled and accessToken filled and `TVHTML5EMBEDDED` to be removed from oauth clients, since it's the default config 59 | - `**` = to do that, you need to add `source` option into `SearchOptions`. Example: `kazagumo.search(query, {source: "spsearch:"});` (⚠️you need to include `:` in the last of `spsearch` or anything to replace source) 60 | 61 | ## Plugins 62 | - Official [spotify plugin](https://npmjs.com/package/kazagumo-spotify) 63 | > npm i kazagumo-spotify 64 | - Additional [apple plugin](https://www.npmjs.com/package/kazagumo-apple) 65 | > npm i kazagumo-apple 66 | - Additional [filter plugin](https://www.npmjs.com/package/kazagumo-filter) 67 | > npm i kazagumo-filter 68 | - Additional [nicovideo.jp plugin](https://www.npmjs.com/package/kazagumo-nico) 69 | > npm i kazagumo-nico 70 | - Additional [deezer plugin](https://www.npmjs.com/package/kazagumo-deezer) 71 | > npm i kazagumo-deezer 72 | - Stone-Deezer [deezer plugin](https://www.npmjs.com/package/stone-deezer) 73 | > npm i stone-deezer 74 | 75 | ## Lavalink installation 76 | > Basically you can follow this [Official Step](https://lavalink.dev/getting-started/index.html) 77 | 78 | 79 | ## Changes v2 -> v3 80 | ```javascript 81 | // You can get ShoukakuPlayer from here 82 | + .shoukaku 83 | + this.player.players.get("69696969696969").shoukaku 84 | 85 | // Search tracks 86 | - this.player.getNode().rest.resolve("ytsearch:pretender Official髭男dism") // Shoukaku 87 | + this.player.search("pretender Official髭男dism") // Kazagumo 88 | 89 | // Create a player 90 | - this.player.getNode().joinChannel(...) // Shoukaku 91 | + this.player.createPlayer(...) // Kazagumo 92 | 93 | // Add a track to the queue. MUST BE A kazagumoTrack, you can get from .search() 94 | + this.player.players.get("69696969696969").queue.add(kazagumoTrack) // Kazagumo 95 | 96 | // Play a track 97 | - this.player.players.get("69696969696969").playTrack(shoukakuTrack) // Shoukaku 98 | + this.player.players.get("69696969696969").play() // Kazagumo, take the first song on queue 99 | + this.player.players.get("69696969696969").play(kazagumoTrack) // Kazagumo, will unshift current song and forceplay this song 100 | 101 | // Play previous song 102 | + this.player.players.get("69696969696969").play(this.player.players.get("69696969696969").getPrevious()) // Kazagumo, make sure it's not undefined first 103 | 104 | // Pauses or resumes the player. Control from kazagumoPlayer instead of shoukakuPlayer 105 | - this.player.players.get("69696969696969").setPaused(true) // Shoukaku 106 | + this.player.players.get("69696969696969").pause(true) // Kazagumo 107 | 108 | // Set filters. Access shoukakuPlayer from .player 109 | - this.player.players.get("69696969696969").setFilters({lowPass: {smoothing: 2}}) // Shoukaku 110 | + this.player.players.get("69696969696969").shoukaku.setFilters({lowPass: {smoothing: 2}}) // Kazagumo 111 | 112 | // Set volume, use Kazagumo's for smoother volume 113 | - this.player.players.get("69696969696969").setVolume(1) // Shoukaku 100% volume 114 | + this.player.players.get("69696969696969").setVolume(100) // Kazagumo 100% volume 115 | 116 | // Skip the current song 117 | - this.player.players.get("69696969696969").stopTrack() // Stoptrack basically skip on shoukaku 118 | + this.player.players.get("69696969696969").skip() // skip on kazagumo. easier to find :v 119 | ``` 120 | 121 | ## Support 122 | ⚠️ Please read the docs first before asking question ⚠️ 123 | > Kazagumo support server: https://discord.gg/nPPW2Gzqg2 (anywhere lmao) 124 | > Shoukaku support server: https://discord.gg/FVqbtGu (#development) 125 | > Report if you found a bug here https://github.com/Takiyo0/Kazagumo/issues/new/choose 126 | 127 | ## Enable playerMoved event 128 | ```javascript 129 | import { Kazagumo, Payload, Plugins } from "kazagumo"; 130 | 131 | const kazagumo = new Kazagumo({ 132 | ..., 133 | plugins: [new Plugins.PlayerMoved(client)] 134 | }, Connector, Nodes, ShoukakuOptions) 135 | ``` 136 | 137 | ## Example bot 138 | ```javascript 139 | const {Client, GatewayIntentBits} = require('discord.js'); 140 | const {Guilds, GuildVoiceStates, GuildMessages, MessageContent} = GatewayIntentBits; 141 | const {Connectors} = require("shoukaku"); 142 | const {Kazagumo, KazagumoTrack} = require("../dist"); 143 | 144 | const Nodes = [{ 145 | name: 'owo', 146 | url: 'localhost:2333', 147 | auth: 'youshallnotpass', 148 | secure: false 149 | }]; 150 | const client = new Client({intents: [Guilds, GuildVoiceStates, GuildMessages, MessageContent]}); 151 | const kazagumo = new Kazagumo({ 152 | defaultSearchEngine: "youtube", 153 | // MAKE SURE YOU HAVE THIS 154 | send: (guildId, payload) => { 155 | const guild = client.guilds.cache.get(guildId); 156 | if (guild) guild.shard.send(payload); 157 | } 158 | }, new Connectors.DiscordJS(client), Nodes); 159 | 160 | client.on("ready", () => console.log(client.user.tag + " Ready!")); 161 | 162 | kazagumo.shoukaku.on('ready', (name) => console.log(`Lavalink ${name}: Ready!`)); 163 | kazagumo.shoukaku.on('error', (name, error) => console.error(`Lavalink ${name}: Error Caught,`, error)); 164 | kazagumo.shoukaku.on('close', (name, code, reason) => console.warn(`Lavalink ${name}: Closed, Code ${code}, Reason ${reason || 'No reason'}`)); 165 | kazagumo.shoukaku.on('debug', (name, info) => console.debug(`Lavalink ${name}: Debug,`, info)); 166 | kazagumo.shoukaku.on('disconnect', (name, count) => { 167 | const players = [...kazagumo.shoukaku.players.values()].filter(p => p.node.name === name); 168 | players.map(player => { 169 | kazagumo.destroyPlayer(player.guildId); 170 | player.destroy(); 171 | }); 172 | console.warn(`Lavalink ${name}: Disconnected`); 173 | }); 174 | 175 | kazagumo.on("playerStart", (player, track) => { 176 | client.channels.cache.get(player.textId)?.send({content: `Now playing **${track.title}** by **${track.author}**`}) 177 | .then(x => player.data.set("message", x)); 178 | }); 179 | 180 | kazagumo.on("playerEnd", (player) => { 181 | player.data.get("message")?.edit({content: `Finished playing`}); 182 | }); 183 | 184 | kazagumo.on("playerEmpty", player => { 185 | client.channels.cache.get(player.textId)?.send({content: `Destroyed player due to inactivity.`}) 186 | .then(x => player.data.set("message", x)); 187 | player.destroy(); 188 | }); 189 | 190 | client.on("messageCreate", async msg => { 191 | if (msg.author.bot) return; 192 | 193 | if (msg.content.startsWith("!play")) { 194 | const args = msg.content.split(" "); 195 | const query = args.slice(1).join(" "); 196 | 197 | const {channel} = msg.member.voice; 198 | if (!channel) return msg.reply("You need to be in a voice channel to use this command!"); 199 | 200 | let player = await kazagumo.createPlayer({ 201 | guildId: msg.guild.id, 202 | textId: msg.channel.id, 203 | voiceId: channel.id, 204 | volume: 40 205 | }) 206 | 207 | let result = await kazagumo.search(query, {requester: msg.author}); 208 | if (!result.tracks.length) return msg.reply("No results found!"); 209 | 210 | if (result.type === "PLAYLIST") player.queue.add(result.tracks); // do this instead of using for loop if you want queueUpdate not spammy 211 | else player.queue.add(result.tracks[0]); 212 | 213 | if (!player.playing && !player.paused) player.play(); 214 | return msg.reply({content: result.type === "PLAYLIST" ? `Queued ${result.tracks.length} from ${result.playlistName}` : `Queued ${result.tracks[0].title}`}); 215 | } 216 | 217 | if (msg.content.startsWith("!skip")) { 218 | let player = kazagumo.players.get(msg.guild.id); 219 | if (!player) return msg.reply("No player found!"); 220 | player.skip(); 221 | log(msg.guild.id); 222 | return msg.reply({content: `Skipped to **${player.queue[0]?.title}** by **${player.queue[0]?.author}**`}); 223 | } 224 | 225 | if (msg.content.startsWith("!forceplay")) { 226 | let player = kazagumo.players.get(msg.guild.id); 227 | if (!player) return msg.reply("No player found!"); 228 | const args = msg.content.split(" "); 229 | const query = args.slice(1).join(" "); 230 | let result = await kazagumo.search(query, {requester: msg.author}); 231 | if (!result.tracks.length) return msg.reply("No results found!"); 232 | player.play(new KazagumoTrack(result.tracks[0].getRaw(), msg.author)); 233 | return msg.reply({content: `Forced playing **${result.tracks[0].title}** by **${result.tracks[0].author}**`}); 234 | } 235 | 236 | if (msg.content.startsWith("!previous")) { 237 | let player = kazagumo.players.get(msg.guild.id); 238 | if (!player) return msg.reply("No player found!"); 239 | const previous = player.getPrevious(); // we get the previous track without removing it first 240 | if (!previous) return msg.reply("No previous track found!"); 241 | await player.play(player.getPrevious(true)); // now we remove the previous track and play it 242 | return msg.reply("Previous!"); 243 | } 244 | }) 245 | 246 | 247 | client.login(''); 248 | ``` 249 | 250 | ## Known issue 251 | ###### This part should be in kazagumo-spotify but whatever 252 | - Force playing song from spotify module (player.play(result.tracks[0]); `result.tracks[0]` is from spotify) is currently not working. **ONLY WHEN YOU DO player.play(thing), NOT player.play() OR player.queue.add(new KazagumoTrack(...))** Please use this workaround 253 | ```js 254 | const { KazagumoTrack } = require("kazagumo"); // CommonJS 255 | import { KazagumoTrack } from "kazagumo"; // ES6; don't laugh if it's wrong 256 | 257 | let track = result.tracks[0] // the spotify track 258 | let convertedTrack = new KazagumoTrack(track.getRaw()._raw, track.author); 259 | player.play(convertedTrack); 260 | ``` 261 | 262 | ## Contributors 263 | > - Deivu as the owner of Shoukaku 264 | > Github: https://github.com/Deivu 265 | > 266 | > - Takiyo as the owner of this project 267 | > Github: https://github.com/Takiyo0 268 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "16" 4 | 5 | # Install scripts. (runs after repo cloning) 6 | install: 7 | # Get the latest stable version of Node.js or io.js 8 | - ps: Install-Product node $env:nodejs_version 9 | # install modules 10 | - npm install 11 | 12 | # Post-install test scripts. 13 | test_script: 14 | # Output useful info for debugging. 15 | - node --version 16 | - npm --version 17 | # run tests 18 | - npm run lint 19 | - npm run format 20 | - npm run build 21 | - npm run build:docs 22 | 23 | # Don't actually build. 24 | build: off 25 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #008000; 3 | --dark-hl-0: #6A9955; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #795E26; 11 | --dark-hl-4: #DCDCAA; 12 | --light-hl-5: #A31515; 13 | --dark-hl-5: #CE9178; 14 | --light-hl-6: #098658; 15 | --dark-hl-6: #B5CEA8; 16 | --light-hl-7: #AF00DB; 17 | --dark-hl-7: #C586C0; 18 | --light-hl-8: #0070C1; 19 | --dark-hl-8: #4FC1FF; 20 | --light-hl-9: #000000FF; 21 | --dark-hl-9: #D4D4D4; 22 | --light-code-background: #FFFFFF; 23 | --dark-code-background: #1E1E1E; 24 | } 25 | 26 | @media (prefers-color-scheme: light) { :root { 27 | --hl-0: var(--light-hl-0); 28 | --hl-1: var(--light-hl-1); 29 | --hl-2: var(--light-hl-2); 30 | --hl-3: var(--light-hl-3); 31 | --hl-4: var(--light-hl-4); 32 | --hl-5: var(--light-hl-5); 33 | --hl-6: var(--light-hl-6); 34 | --hl-7: var(--light-hl-7); 35 | --hl-8: var(--light-hl-8); 36 | --hl-9: var(--light-hl-9); 37 | --code-background: var(--light-code-background); 38 | } } 39 | 40 | @media (prefers-color-scheme: dark) { :root { 41 | --hl-0: var(--dark-hl-0); 42 | --hl-1: var(--dark-hl-1); 43 | --hl-2: var(--dark-hl-2); 44 | --hl-3: var(--dark-hl-3); 45 | --hl-4: var(--dark-hl-4); 46 | --hl-5: var(--dark-hl-5); 47 | --hl-6: var(--dark-hl-6); 48 | --hl-7: var(--dark-hl-7); 49 | --hl-8: var(--dark-hl-8); 50 | --hl-9: var(--dark-hl-9); 51 | --code-background: var(--dark-code-background); 52 | } } 53 | 54 | :root[data-theme='light'] { 55 | --hl-0: var(--light-hl-0); 56 | --hl-1: var(--light-hl-1); 57 | --hl-2: var(--light-hl-2); 58 | --hl-3: var(--light-hl-3); 59 | --hl-4: var(--light-hl-4); 60 | --hl-5: var(--light-hl-5); 61 | --hl-6: var(--light-hl-6); 62 | --hl-7: var(--light-hl-7); 63 | --hl-8: var(--light-hl-8); 64 | --hl-9: var(--light-hl-9); 65 | --code-background: var(--light-code-background); 66 | } 67 | 68 | :root[data-theme='dark'] { 69 | --hl-0: var(--dark-hl-0); 70 | --hl-1: var(--dark-hl-1); 71 | --hl-2: var(--dark-hl-2); 72 | --hl-3: var(--dark-hl-3); 73 | --hl-4: var(--dark-hl-4); 74 | --hl-5: var(--dark-hl-5); 75 | --hl-6: var(--dark-hl-6); 76 | --hl-7: var(--dark-hl-7); 77 | --hl-8: var(--dark-hl-8); 78 | --hl-9: var(--dark-hl-9); 79 | --code-background: var(--dark-code-background); 80 | } 81 | 82 | .hl-0 { color: var(--hl-0); } 83 | .hl-1 { color: var(--hl-1); } 84 | .hl-2 { color: var(--hl-2); } 85 | .hl-3 { color: var(--hl-3); } 86 | .hl-4 { color: var(--hl-4); } 87 | .hl-5 { color: var(--hl-5); } 88 | .hl-6 { color: var(--hl-6); } 89 | .hl-7 { color: var(--hl-7); } 90 | .hl-8 { color: var(--hl-8); } 91 | .hl-9 { color: var(--hl-9); } 92 | pre, code { background: var(--code-background); } 93 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAACp2Y207bQBCG38W9jQqEQCG3kAtUoVKglSoURYs9JBbOOlqvU9KKd6+I48Oudw7uVaTkn+/fGe+uZ/L0N7LwZqNpdKMTeItG0UbZVTSN1nlSZlAc7b/+vLLrLBpFr6lOoul4FMWrNEsM6Gj61ACuDCgLd5nagfm2sWmuCwr3KayvLCYnl5PT48n7qKHPtqAtDWwkGOOr+qOW5TonKR0Rx5kZkxsRrFayRD7LnpRjSp5FX8tRq+cmgjZSGXPIeqW7p9WXy1QL0Qcpx/xeQgkiZK3kiA+gTLwaUgc/QuZwD0WZ2QEGTQDHfzQqfhWBayVGvFO7LFcJyWo1KCVTO0k9XR1FA3ObbyG5WimtIeOpfb2A/mCVpbdWQExzpUie9nE8uLxrDUa5V7/5rdIRoRwo8mwLkkfck2LMasvP9DLVQCN9JU2sDtHjbiOiumqUnJcmhptrhthRoSR2f3A746HcbHJjIansmCX1xRj3Z57GwC/PkWGsX3lpy2d4XJXrZ63SjCQGxBgXilht4B6Ws7cNyfSEGG8Lpkjzzgtrq0yqnlvYQeA2aKfj97mo7al/EfV3fUycqaLoYsK8k/GFoNVJtQXzouIQr9J6qzw7d7K8VVotwbTRWIdSCxeucFANfHZdCYxNWX2UJ5jI4Wy0GSGdRmNaByycgEGJeRa9vBALwlCeHvImwL33AYPS8yz49CoLwrCXXnPW6+0cyKj6XLQaURLBFzjoch0kdtQu/QK/7VEYgwldzSirFeNAZMJqnlef6kRI7h9/FBCwqxAKTs7AnRsuYBII7V94gumOdvHCJA7/lU0wWOKGzD4yNydY7ubPQUPMqljKqzfB0PiDnCSGphmG2oZwZHSy4R28UMqp3/nT+FpPMpEpgCE7UcLqeJed/ejPmaoErrzx8eWXk7MxP3SgfEcvgwfnD8agE8OZ4K016uGHcBZ+69g2xQF4qHc8HdOzEwls9BQTnYFotBdGOIQnjZdSx/ttHKJ3Q1zy+STYw6Azfg0/CETdSwIvyrll+5WocQcpOdTUa/xh09C/LzVy//OgFtED+m1BBXTEZEOQ68KaMrbdNsY9CBWjIwxv/7lzEe0LddS5XfolOIgWHdHAGSfcIIW4XIc0f5//A+xLBqdqGAAA" -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = "data:application/octet-stream;base64,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"; -------------------------------------------------------------------------------- /docs/modules/Managers_KazagumoPlayer.html: -------------------------------------------------------------------------------- 1 | Managers/KazagumoPlayer | kazagumoPreparing search index...The search index is not availablekazagumokazagumoManagers/KazagumoPlayerModule Managers/KazagumoPlayerIndexClassesKazagumoPlayer 2 | SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /docs/modules/Managers_Supports_KazagumoQueue.html: -------------------------------------------------------------------------------- 1 | Managers/Supports/KazagumoQueue | kazagumoPreparing search index...The search index is not availablekazagumokazagumoManagers/Supports/KazagumoQueueModule Managers/Supports/KazagumoQueueIndexClassesKazagumoQueue 2 | SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /docs/modules/Managers_Supports_KazagumoTrack.html: -------------------------------------------------------------------------------- 1 | Managers/Supports/KazagumoTrack | kazagumoPreparing search index...The search index is not availablekazagumokazagumoManagers/Supports/KazagumoTrackModule Managers/Supports/KazagumoTrackIndexClassesKazagumoTrack 2 | SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /docs/modules/Modules_Plugins.html: -------------------------------------------------------------------------------- 1 | Modules/Plugins | kazagumoPreparing search index...The search index is not availablekazagumokazagumoModules/PluginsModule Modules/PluginsIndexVariablesdefault 2 | SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /docs/modules/Plugins_PlayerMoved.html: -------------------------------------------------------------------------------- 1 | Plugins/PlayerMoved | kazagumoPreparing search index...The search index is not availablekazagumokazagumoPlugins/PlayerMovedModule Plugins/PlayerMovedIndexClassesKazagumoPlugin 2 | SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /docs/variables/Index.version.html: -------------------------------------------------------------------------------- 1 | version | kazagumoPreparing search index...The search index is not availablekazagumokazagumoIndexversionVariable versionConst version: "3.3.0" = '3.3.0'SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /docs/variables/Modules_Interfaces.SupportedSources.html: -------------------------------------------------------------------------------- 1 | SupportedSources | kazagumoPreparing search index...The search index is not availablekazagumokazagumoModules/InterfacesSupportedSourcesVariable SupportedSourcesConst SupportedSources: string[] = ...SettingsMember VisibilityProtectedPrivateInheritedExternalThemeOSLightDarkkazagumoIndexKazagumoManagers/KazagumoPlayerManagers/Supports/KazagumoQueueManagers/Supports/KazagumoTrackModules/InterfacesModules/PluginsModules/UtilsPlugins/PlayerMovedGenerated using TypeDoc -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kazagumo", 3 | "version": "3.3.0", 4 | "description": "A shoukaku wrapper with built-in queue support.", 5 | "main": "dist/Index.js", 6 | "types": "dist/Index.d.ts", 7 | "homepage": "https://github.com/Takiyo0/Kazagumo", 8 | "readme": "https://github.com/Takiyo0/Kazagumo#readme", 9 | "bugs": { 10 | "url": "https://github.com/Takiyo0/Kazagumo", 11 | "email": "takiyotakahashi@gmail.com" 12 | }, 13 | "engines": { 14 | "node": ">=16.5.0" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1", 18 | "build:full": "npm run lint && npm run format && npm run build:docs && npm run build", 19 | "build": "tsc", 20 | "format": "prettier --write \"src/**/**/*.ts\"", 21 | "lint": "tslint -p tsconfig.json", 22 | "build:docs": "typedoc --readme README.md --out docs/ --entryPointStrategy expand src/." 23 | }, 24 | "keywords": [ 25 | "lavalink", 26 | "lavalink-client", 27 | "shoukaku", 28 | "kazagumo", 29 | "lavalink-player", 30 | "player", 31 | "discord-player" 32 | ], 33 | "author": "Takiyo", 34 | "files": [ 35 | "dist/**/**/*" 36 | ], 37 | "license": "ISC", 38 | "devDependencies": { 39 | "@types/events": "^3.0.0", 40 | "@types/node": "^18.11.9", 41 | "@types/ws": "^8.5.3", 42 | "@typescript-eslint/eslint-plugin": "^6.15.0", 43 | "@typescript-eslint/parser": "^6.15.0", 44 | "discord.js": "^14.15.3", 45 | "events": "^3.3.0", 46 | "kazagumo-spotify": "file:../kazagumo-spotify", 47 | "prettier": "^3.1.1", 48 | "rebuild": "^0.1.2", 49 | "reinstall": "^2.0.0", 50 | "tslint": "^6.1.3", 51 | "tslint-config-prettier": "^1.18.0", 52 | "typedoc": "^0.25.4", 53 | "typescript": "^5.3.3" 54 | }, 55 | "dependencies": { 56 | "shoukaku": "^4.1.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Index.ts: -------------------------------------------------------------------------------- 1 | // import { NodeOption, PlayerUpdate, ShoukakuOptions, TrackExceptionEvent, WebSocketClosedEvent } from "shoukaku"; 2 | import { KazagumoTrack } from './Managers/Supports/KazagumoTrack'; 3 | import { KazagumoQueue } from './Managers/Supports/KazagumoQueue'; 4 | import { KazagumoPlayer } from './Managers/KazagumoPlayer'; 5 | import Plugins from './Modules/Plugins'; 6 | // import KazagumoPlayer from "./Managers/KazagumoPlayer"; 7 | // import { KazagumoOptions } from "./Modules/Interfaces"; 8 | // import { Connector } from "shoukaku/dist/src/connectors/Connector"; 9 | 10 | export * from './Kazagumo'; 11 | export { KazagumoTrack, KazagumoQueue, KazagumoPlayer, Plugins }; 12 | export * from './Modules/Interfaces'; 13 | 14 | export const version = '3.3.0'; 15 | -------------------------------------------------------------------------------- /src/Kazagumo.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { 3 | CreatePlayerOptions, 4 | Events, 5 | KazagumoError, 6 | KazagumoOptions as KazagumoOptionsOwO, 7 | KazagumoSearchOptions, 8 | KazagumoSearchResult, 9 | PlayerMovedChannels, 10 | PlayerMovedState, 11 | SearchResultTypes, 12 | SourceIDs, 13 | State, 14 | VoiceState, 15 | } from './Modules/Interfaces'; 16 | import { 17 | Connection, 18 | Connector, 19 | LoadType, 20 | Node, 21 | NodeOption, 22 | Player, 23 | PlayerUpdate, 24 | Shoukaku, 25 | ShoukakuOptions, 26 | Track, 27 | TrackExceptionEvent, 28 | TrackStuckEvent, 29 | VoiceChannelOptions, 30 | WebSocketClosedEvent, 31 | } from 'shoukaku'; 32 | 33 | import { KazagumoPlayer } from './Managers/KazagumoPlayer'; 34 | import { KazagumoTrack } from './Managers/Supports/KazagumoTrack'; 35 | import { KazagumoQueue } from './Managers/Supports/KazagumoQueue'; 36 | 37 | export interface KazagumoEvents { 38 | /** 39 | * Emitted when a track is going to play. 40 | * @event Kazagumo#playerStart 41 | */ 42 | playerStart: [player: KazagumoPlayer, track: KazagumoTrack]; 43 | 44 | /** 45 | * Emitted when an error occured while resolving track. 46 | * @event Kazagumo#playerResolveError 47 | */ 48 | playerResolveError: [player: KazagumoPlayer, track: KazagumoTrack, message?: string]; 49 | 50 | /** 51 | * Emitted when a player got destroyed. 52 | * @event Kazagumo#playerDestroy 53 | */ 54 | playerDestroy: [player: KazagumoPlayer]; 55 | 56 | /** 57 | * Emitted when a player created. 58 | * @event Kazagumo#playerCreate 59 | */ 60 | playerCreate: [player: KazagumoPlayer]; 61 | 62 | /** 63 | * Emitted when a track ended. 64 | * @event Kazagumo#playerEnd 65 | */ 66 | playerEnd: [player: KazagumoPlayer]; 67 | 68 | /** 69 | * Emitted when a player got empty. 70 | * @event Kazagumo#playerEmpty 71 | */ 72 | playerEmpty: [player: KazagumoPlayer]; 73 | 74 | /** 75 | * Emitted when a player got closed. 76 | * @event Kazagumo#playerClosed 77 | */ 78 | playerClosed: [player: KazagumoPlayer, data: WebSocketClosedEvent]; 79 | 80 | /** 81 | * Emitted when a player got stuck. 82 | * @event Kazagumo#playerStuck 83 | */ 84 | playerStuck: [player: KazagumoPlayer, data: TrackStuckEvent]; 85 | 86 | /** 87 | * Emitted when a player got resumed. 88 | * @event Kazagumo#playerResumed 89 | */ 90 | playerResumed: [player: KazagumoPlayer]; 91 | 92 | /** 93 | * Emitted only when you use playerMoved plugin and when the bot moved, joined, or left voice channel. 94 | * @event Kazagumo#playerMoved 95 | */ 96 | playerMoved: [player: KazagumoPlayer, state: PlayerMovedState, channels: PlayerMovedChannels]; 97 | 98 | /** 99 | * Emitted when an exception occured. 100 | * @event Kazagumo#playerException 101 | */ 102 | playerException: [player: KazagumoPlayer, data: TrackExceptionEvent]; 103 | 104 | /** 105 | * Emitted when a player updated. 106 | * @event Kazagumo#playerUpdate 107 | */ 108 | playerUpdate: [player: KazagumoPlayer, data: PlayerUpdate]; 109 | 110 | /** 111 | * Emitted for science purpose. 112 | * @event Kazagumo#playerUpdate 113 | */ 114 | // maybe this should be changed 115 | // 'playerUpdate': [data: unknown]; 116 | 117 | /** 118 | * Emitted when a queue updated (track added, changed, etc). 119 | * @event Kazagumo#queueUpdate 120 | */ 121 | queueUpdate: [player: KazagumoPlayer, queue: KazagumoQueue]; 122 | } 123 | 124 | export declare interface Kazagumo { 125 | on(event: K, listener: (...args: KazagumoEvents[K]) => void): this; 126 | once(event: K, listener: (...args: KazagumoEvents[K]) => void): this; 127 | off(event: K, listener: (...args: KazagumoEvents[K]) => void): this; 128 | emit(event: string | symbol, ...args: any[]): boolean; 129 | } 130 | 131 | export class Kazagumo extends EventEmitter { 132 | /** Shoukaku instance */ 133 | public shoukaku: Shoukaku; 134 | /** Kazagumo players */ 135 | public readonly players: Map = new Map(); 136 | 137 | /** 138 | * Initialize a Kazagumo instance. 139 | * @param KazagumoOptions KazagumoOptions 140 | * @param connector Connector 141 | * @param nodes NodeOption[] 142 | * @param options ShoukakuOptions 143 | */ 144 | constructor( 145 | public KazagumoOptions: KazagumoOptionsOwO, 146 | connector: Connector, 147 | nodes: NodeOption[], 148 | options: ShoukakuOptions = {}, 149 | ) { 150 | super(); 151 | 152 | this.shoukaku = new Shoukaku(connector, nodes, options); 153 | 154 | if (this.KazagumoOptions.plugins) { 155 | for (const [, plugin] of this.KazagumoOptions.plugins.entries()) { 156 | if (plugin.constructor.name !== 'KazagumoPlugin') 157 | throw new KazagumoError(1, 'Plugin must be an instance of KazagumoPlugin'); 158 | plugin.load(this); 159 | } 160 | } 161 | 162 | this.players = new Map(); 163 | } 164 | 165 | // Modified version of Shoukaku#joinVoiceChannel 166 | // Credit to @deivu 167 | protected async createVoiceConnection( 168 | newPlayerOptions: VoiceChannelOptions, 169 | kazagumoPlayerOptions: CreatePlayerOptions, 170 | ): Promise { 171 | if (this.shoukaku.connections.has(newPlayerOptions.guildId) && this.shoukaku.players.has(newPlayerOptions.guildId)) 172 | return this.shoukaku.players.get(newPlayerOptions.guildId)!; 173 | if ( 174 | this.shoukaku.connections.has(newPlayerOptions.guildId) && 175 | !this.shoukaku.players.has(newPlayerOptions.guildId) 176 | ) { 177 | this.shoukaku.connections.get(newPlayerOptions.guildId)!.disconnect(); 178 | // tslint:disable-next-line:no-console 179 | console.log( 180 | '[KazagumoError; l220 Kazagumo.ts] -> Connection exist but player not found. Destroying connection...', 181 | ); 182 | } 183 | 184 | const connection = new Connection(this.shoukaku, newPlayerOptions); 185 | this.shoukaku.connections.set(connection.guildId, connection); 186 | try { 187 | await connection.connect(); 188 | } catch (error) { 189 | this.shoukaku.connections.delete(newPlayerOptions.guildId); 190 | throw error; 191 | } 192 | try { 193 | let node; 194 | if (kazagumoPlayerOptions.loadBalancer) node = await this.getLeastUsedNode(); 195 | else if (kazagumoPlayerOptions.nodeName) 196 | node = this.shoukaku.nodes.get(kazagumoPlayerOptions.nodeName) ?? (await this.getLeastUsedNode()); 197 | else node = this.shoukaku.options.nodeResolver(this.shoukaku.nodes); 198 | if (!node) throw new KazagumoError(3, 'No node found'); 199 | 200 | const player = this.shoukaku.options.structures.player 201 | ? new this.shoukaku.options.structures.player(connection.guildId, node) 202 | : new Player(connection.guildId, node); 203 | const onUpdate = (state: VoiceState) => { 204 | if (state !== VoiceState.SESSION_READY) return; 205 | player.sendServerUpdate(connection); 206 | }; 207 | await player.sendServerUpdate(connection); 208 | connection.on('connectionUpdate', onUpdate); 209 | this.shoukaku.players.set(player.guildId, player); 210 | return player; 211 | } catch (error) { 212 | connection.disconnect(); 213 | this.shoukaku.connections.delete(newPlayerOptions.guildId); 214 | throw error; 215 | } 216 | } 217 | 218 | /** 219 | * Create a player. 220 | * @param options CreatePlayerOptions 221 | * @returns Promise 222 | */ 223 | public async createPlayer(options: CreatePlayerOptions): Promise { 224 | const exist = this.players.get(options.guildId); 225 | if (exist) return exist; 226 | 227 | let node; 228 | if (options.loadBalancer) node = this.getLeastUsedNode(); 229 | else if (options.nodeName) node = this.shoukaku.nodes.get(options.nodeName) ?? this.getLeastUsedNode(); 230 | else node = this.shoukaku.options.nodeResolver(this.shoukaku.nodes); 231 | 232 | if (!options.deaf) options.deaf = false; 233 | if (!options.mute) options.mute = false; 234 | 235 | if (!node) throw new KazagumoError(3, 'No node found'); 236 | 237 | const shoukakuPlayer = await this.createVoiceConnection( 238 | { 239 | guildId: options.guildId as string, 240 | channelId: options.voiceId as string, 241 | deaf: options.deaf, 242 | mute: options.mute, 243 | shardId: options.shardId && !isNaN(options.shardId) ? options.shardId : 0, 244 | }, 245 | options, 246 | ); 247 | 248 | const kazagumoPlayer = new (this.KazagumoOptions.extends?.player ?? KazagumoPlayer)( 249 | this, 250 | shoukakuPlayer, 251 | { 252 | guildId: options.guildId, 253 | voiceId: options.voiceId, 254 | textId: options.textId, 255 | deaf: options.deaf, 256 | volume: isNaN(Number(options.volume)) ? 100 : (options.volume as number), 257 | }, 258 | options.data, 259 | ); 260 | this.players.set(options.guildId, kazagumoPlayer); 261 | this.emit(Events.PlayerCreate, kazagumoPlayer); 262 | return kazagumoPlayer; 263 | } 264 | 265 | /** 266 | * Get a player by guildId. 267 | * @param guildId Guild ID 268 | * @returns KazagumoPlayer | undefined 269 | */ 270 | public getPlayer(guildId: string): (T | KazagumoPlayer) | undefined { 271 | return this.players.get(guildId); 272 | } 273 | 274 | /** 275 | * Destroy a player. 276 | * @param guildId Guild ID 277 | * @returns void 278 | */ 279 | public destroyPlayer(guildId: string): void { 280 | const player = this.getPlayer(guildId); 281 | if (!player) return; 282 | player.destroy(); 283 | this.players.delete(guildId); 284 | } 285 | 286 | /** 287 | * Get the least used node. 288 | * @param group The group where you want to get the least used nodes there. Case-sensitive, catch the error when there is no such group 289 | * @returns Node 290 | */ 291 | public async getLeastUsedNode(group?: string): Promise { 292 | const nodes: Node[] = [...this.shoukaku.nodes.values()]; 293 | 294 | const onlineNodes = nodes.filter((node) => node.state === State.CONNECTED && (!group || node.group === group)); 295 | if (!onlineNodes.length && group && !nodes.find((x) => x.group === group)) 296 | throw new KazagumoError(2, `There is no such group: ${group}`); 297 | if (!onlineNodes.length) 298 | throw new KazagumoError(2, !!group ? `No nodes are online in ${group}` : 'No nodes are online'); 299 | 300 | const temp = await Promise.all( 301 | onlineNodes.map(async (node) => ({ 302 | node, 303 | players: (await node.rest.getPlayers()) 304 | .filter((x) => this.players.get(x.guildId)) 305 | .map((x) => this.players.get(x.guildId)!) 306 | .filter((x) => x.shoukaku.node.name === node.name).length, 307 | })), 308 | ); 309 | 310 | return temp.reduce((a, b) => (a.players < b.players ? a : b)).node; 311 | } 312 | 313 | /** 314 | * Search a track by query or uri. 315 | * @param query Query 316 | * @param options KazagumoOptions 317 | * @returns Promise 318 | */ 319 | public async search(query: string, options?: KazagumoSearchOptions): Promise { 320 | const node = options?.nodeName 321 | ? this.shoukaku.nodes.get(options.nodeName) ?? (await this.getLeastUsedNode()) 322 | : await this.getLeastUsedNode(); 323 | if (!node) throw new KazagumoError(3, 'No node is available'); 324 | 325 | const source = (SourceIDs as any)[ 326 | (options?.engine && ['youtube', 'youtube_music', 'soundcloud'].includes(options.engine) 327 | ? options.engine 328 | : null) || 329 | (!!this.KazagumoOptions.defaultSearchEngine && 330 | ['youtube', 'youtube_music', 'soundcloud'].includes(this.KazagumoOptions.defaultSearchEngine!) 331 | ? this.KazagumoOptions.defaultSearchEngine 332 | : null) || 333 | 'youtube' 334 | ]; 335 | 336 | const isUrl = /^https?:\/\/.*/.test(query); 337 | const customSource = options?.source ?? this.KazagumoOptions.defaultSource ?? `${source}search:`; 338 | 339 | const result = await node.rest.resolve(!isUrl ? `${customSource}${query}` : query).catch((_) => null); 340 | if (!result || result.loadType === LoadType.EMPTY) return this.buildSearch(undefined, [], 'SEARCH'); 341 | 342 | let loadType: SearchResultTypes; 343 | let normalizedData: { 344 | playlistName?: string; 345 | tracks: Track[]; 346 | } = { tracks: [] }; 347 | switch (result.loadType) { 348 | case LoadType.TRACK: { 349 | loadType = 'TRACK'; 350 | normalizedData.tracks = [result.data]; 351 | break; 352 | } 353 | 354 | case LoadType.PLAYLIST: { 355 | loadType = 'PLAYLIST'; 356 | normalizedData = { 357 | playlistName: result.data.info.name, 358 | tracks: result.data.tracks, 359 | }; 360 | break; 361 | } 362 | 363 | case LoadType.SEARCH: { 364 | loadType = 'SEARCH'; 365 | normalizedData.tracks = result.data; 366 | break; 367 | } 368 | 369 | default: { 370 | loadType = 'SEARCH'; 371 | normalizedData.tracks = []; 372 | break; 373 | } 374 | } 375 | this.emit(Events.Debug, `Searched ${query}; Track results: ${normalizedData.tracks.length}`); 376 | 377 | return this.buildSearch( 378 | normalizedData.playlistName ?? undefined, 379 | normalizedData.tracks.map((track) => new KazagumoTrack(track, options?.requester)), 380 | loadType, 381 | ); 382 | } 383 | 384 | private buildSearch( 385 | playlistName?: string, 386 | tracks: KazagumoTrack[] = [], 387 | type?: SearchResultTypes, 388 | ): KazagumoSearchResult { 389 | return { 390 | playlistName, 391 | tracks, 392 | type: type ?? 'SEARCH', 393 | }; 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/Managers/KazagumoPlayer.ts: -------------------------------------------------------------------------------- 1 | import { Kazagumo } from '../Kazagumo'; 2 | import { KazagumoQueue } from './Supports/KazagumoQueue'; 3 | import { 4 | FilterOptions, 5 | Node, 6 | Player, 7 | PlayerUpdate, 8 | TrackExceptionEvent, 9 | TrackStuckEvent, 10 | WebSocketClosedEvent, 11 | } from 'shoukaku'; 12 | import { 13 | Events, 14 | KazagumoError, 15 | KazagumoPlayerOptions, 16 | KazagumoSearchOptions, 17 | KazagumoSearchResult, 18 | PlayerState, 19 | PlayOptions, 20 | } from '../Modules/Interfaces'; 21 | import { KazagumoTrack } from './Supports/KazagumoTrack'; 22 | 23 | export class KazagumoPlayer { 24 | /** 25 | * Kazagumo options 26 | */ 27 | private options: KazagumoPlayerOptions; 28 | /** 29 | * Kazagumo Instance 30 | */ 31 | public readonly kazagumo: Kazagumo; 32 | /** 33 | * Shoukaku's Player instance 34 | */ 35 | public shoukaku: Player; 36 | /** 37 | * The guild ID of the player 38 | */ 39 | public readonly guildId: string; 40 | /** 41 | * The voice channel ID of the player 42 | */ 43 | public voiceId: string | null; 44 | /** 45 | * The text channel ID of the player 46 | */ 47 | public textId?: string; 48 | /** 49 | * Player's queue 50 | */ 51 | public readonly queue: KazagumoQueue; 52 | /** 53 | * Get the current state of the player 54 | */ 55 | public state: PlayerState = PlayerState.CONNECTING; 56 | /** 57 | * Paused state of the player 58 | */ 59 | public paused: boolean = false; 60 | /** 61 | * Whether the player is playing or not 62 | */ 63 | public playing: boolean = false; 64 | /** 65 | * Loop status 66 | */ 67 | public loop: 'none' | 'queue' | 'track' = 'none'; 68 | /** 69 | * Search track/s 70 | */ 71 | public search: (query: string, options?: KazagumoSearchOptions) => Promise; 72 | /** 73 | * Player's volume in percentage (default 100%) 74 | */ 75 | public volume: number = 100; 76 | /** 77 | * Player's custom data 78 | */ 79 | public readonly data: Map = new Map(); 80 | 81 | /** 82 | * Initialize the player 83 | * @param kazagumo Kazagumo instance 84 | * @param player Shoukaku's Player instance 85 | * @param options Kazagumo options 86 | * @param customData private readonly customData 87 | */ 88 | constructor( 89 | kazagumo: Kazagumo, 90 | player: Player, 91 | options: KazagumoPlayerOptions, 92 | private readonly customData: unknown, 93 | ) { 94 | this.options = options; 95 | this.kazagumo = kazagumo; 96 | this.shoukaku = player; 97 | this.guildId = options.guildId; 98 | this.voiceId = options.voiceId; 99 | this.textId = options.textId; 100 | this.queue = new (this.options.extends?.queue ?? KazagumoQueue)(this); 101 | 102 | if (options.volume !== 100) this.setVolume(options.volume); 103 | 104 | this.search = (typeof this.options.searchWithSameNode === 'boolean' ? this.options.searchWithSameNode : true) 105 | ? (query: string, opt?: KazagumoSearchOptions) => 106 | kazagumo.search.bind(kazagumo)(query, opt ? { ...opt, nodeName: this.shoukaku.node.name } : undefined) 107 | : kazagumo.search.bind(kazagumo); 108 | 109 | this.shoukaku.on('start', () => { 110 | this.playing = true; 111 | this.emit(Events.PlayerStart, this, this.queue.current); 112 | }); 113 | 114 | this.shoukaku.on('end', (data) => { 115 | // This event emits STOPPED reason when destroying, so return to prevent double emit 116 | if (this.state === PlayerState.DESTROYING || this.state === PlayerState.DESTROYED) 117 | return this.emit(Events.Debug, `Player ${this.guildId} destroyed from end event`); 118 | 119 | if (data.reason === 'replaced') return this.emit(Events.PlayerEnd, this); 120 | if (['loadFailed', 'cleanup'].includes(data.reason)) { 121 | if ( 122 | this.queue.current && 123 | !this.queue.previous.find( 124 | (x) => x.identifier === this.queue.current?.identifier && x.title === this.queue.current?.title, 125 | ) 126 | ) 127 | this.queue.previous = [this.queue.current].concat(this.queue.previous); 128 | this.emit(Events.PlayerEnd, this, this.queue.current); 129 | this.queue.current = null; 130 | this.playing = false; 131 | if (!this.queue.length) return this.emit(Events.PlayerEmpty, this); 132 | return this.play(); 133 | } 134 | 135 | if (this.loop === 'track' && this.queue.current) this.queue.unshift(this.queue.current); 136 | if (this.loop === 'queue' && this.queue.current) this.queue.push(this.queue.current); 137 | 138 | if ( 139 | this.queue.current && 140 | !this.queue.previous.find( 141 | (x) => x.identifier === this.queue.current?.identifier && x.title === this.queue.current?.title, 142 | ) 143 | ) 144 | this.queue.previous = [this.queue.current].concat(this.queue.previous); 145 | 146 | const currentSong = this.queue.current; 147 | this.emit(Events.PlayerEnd, this, currentSong); 148 | this.queue.current = null; 149 | 150 | if (!this.queue.length) { 151 | this.playing = false; 152 | return this.emit(Events.PlayerEmpty, this); 153 | } 154 | 155 | return this.play(); 156 | }); 157 | 158 | this.shoukaku.on('closed', (data: WebSocketClosedEvent) => { 159 | this.playing = false; 160 | this.emit(Events.PlayerClosed, this, data); 161 | }); 162 | 163 | this.shoukaku.on('exception', (data: TrackExceptionEvent) => { 164 | this.playing = false; 165 | this.emit(Events.PlayerException, this, data); 166 | }); 167 | 168 | this.shoukaku.on('update', (data: PlayerUpdate) => this.emit(Events.PlayerUpdate, this, data)); 169 | this.shoukaku.on('stuck', (data: TrackStuckEvent) => this.emit(Events.PlayerStuck, this, data)); 170 | this.shoukaku.on('resumed', () => this.emit(Events.PlayerResumed, this)); 171 | // @ts-ignore 172 | this.shoukaku.on(Events.QueueUpdate, (player: KazagumoPlayer, queue: KazagumoQueue) => 173 | this.kazagumo.emit(Events.QueueUpdate, player, queue), 174 | ); 175 | } 176 | 177 | // /** 178 | // * Get volume 179 | // */ 180 | // public get volume(): number { 181 | // return this.shoukaku.filters.volume || 1; 182 | // } 183 | 184 | /** 185 | * Get player position 186 | */ 187 | public get position(): number { 188 | return this.shoukaku.position; 189 | } 190 | 191 | /** 192 | * Get filters 193 | */ 194 | public get filters(): FilterOptions { 195 | return this.shoukaku.filters; 196 | } 197 | 198 | private get node(): Node { 199 | return this.shoukaku.node; 200 | } 201 | 202 | /** 203 | * Pause the player 204 | * @param pause Whether to pause or not 205 | * @returns KazagumoPlayer 206 | */ 207 | public pause(pause: boolean): KazagumoPlayer { 208 | if (typeof pause !== 'boolean') throw new KazagumoError(1, 'pause must be a boolean'); 209 | 210 | if (this.paused === pause || !this.queue.totalSize) return this; 211 | this.paused = pause; 212 | this.playing = !pause; 213 | this.shoukaku.setPaused(pause); 214 | 215 | return this; 216 | } 217 | 218 | /** 219 | * Set text channel 220 | * @param textId Text channel ID 221 | * @returns KazagumoPlayer 222 | */ 223 | public setTextChannel(textId: string): KazagumoPlayer { 224 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 225 | 226 | this.textId = textId; 227 | 228 | return this; 229 | } 230 | 231 | /** 232 | * Set voice channel and move the player to the voice channel 233 | * @param voiceId Voice channel ID 234 | * @returns KazagumoPlayer 235 | */ 236 | public setVoiceChannel(voiceId: string): KazagumoPlayer { 237 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 238 | this.state = PlayerState.CONNECTING; 239 | 240 | this.voiceId = voiceId; 241 | this.kazagumo.KazagumoOptions.send(this.guildId, { 242 | op: 4, 243 | d: { 244 | guild_id: this.guildId, 245 | channel_id: this.voiceId, 246 | self_mute: false, 247 | self_deaf: this.options.deaf, 248 | }, 249 | }); 250 | 251 | this.emit(Events.Debug, `Player ${this.guildId} moved to voice channel ${voiceId}`); 252 | 253 | return this; 254 | } 255 | 256 | /** 257 | * Get the previous track from the queue 258 | * @param remove Whether to remove the track from the previous list or not. Best to set to true if you want to play it 259 | */ 260 | public getPrevious(remove: boolean = false): KazagumoTrack | undefined { 261 | if (remove) return this.queue.previous.shift(); 262 | return this.queue.previous[0]; 263 | } 264 | 265 | /** 266 | * Set loop mode 267 | * @param [loop] Loop mode 268 | * @returns KazagumoPlayer 269 | */ 270 | public setLoop(loop?: 'none' | 'queue' | 'track'): KazagumoPlayer { 271 | if (loop === undefined) { 272 | if (this.loop === 'none') this.loop = 'queue'; 273 | else if (this.loop === 'queue') this.loop = 'track'; 274 | else if (this.loop === 'track') this.loop = 'none'; 275 | return this; 276 | } 277 | 278 | if (loop === 'none' || loop === 'queue' || loop === 'track') { 279 | this.loop = loop; 280 | return this; 281 | } 282 | 283 | throw new KazagumoError(1, "loop must be one of 'none', 'queue', 'track'"); 284 | } 285 | 286 | /** 287 | * Play a track 288 | * @param track Track to play 289 | * @param options Play options 290 | * @returns KazagumoPlayer 291 | */ 292 | public async play(track?: KazagumoTrack, options?: PlayOptions): Promise { 293 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 294 | 295 | if (track && !(track instanceof KazagumoTrack)) throw new KazagumoError(1, 'track must be a KazagumoTrack'); 296 | 297 | if (!track && !this.queue.totalSize) throw new KazagumoError(1, 'No track is available to play'); 298 | 299 | if (!options || typeof options.replaceCurrent !== 'boolean') options = { ...options, replaceCurrent: false }; 300 | 301 | if (track) { 302 | if (!options.replaceCurrent && this.queue.current) this.queue.unshift(this.queue.current); 303 | this.queue.current = track; 304 | } else if (!this.queue.current) this.queue.current = this.queue.shift(); 305 | 306 | if (!this.queue.current) throw new KazagumoError(1, 'No track is available to play'); 307 | 308 | const current = this.queue.current; 309 | current.setKazagumo(this.kazagumo); 310 | 311 | let errorMessage: string | undefined; 312 | 313 | const resolveResult = await current.resolve({ player: this as KazagumoPlayer }).catch((e) => { 314 | errorMessage = e.message; 315 | return null; 316 | }); 317 | 318 | if (!resolveResult) { 319 | this.emit(Events.PlayerResolveError, this, current, errorMessage); 320 | this.emit(Events.Debug, `Player ${this.guildId} resolve error: ${errorMessage}`); 321 | this.queue.current = null; 322 | this.queue.size ? await this.play() : this.emit(Events.PlayerEmpty, this); 323 | return this; 324 | } 325 | 326 | let playOptions = { track: { encoded: current.track, userData: current.requester ?? {} } }; 327 | if (options) playOptions = { ...playOptions, ...options }; 328 | 329 | await this.shoukaku.playTrack(playOptions); 330 | 331 | return this; 332 | } 333 | 334 | /** 335 | * Skip the current track 336 | * @returns KazagumoPlayer 337 | */ 338 | public skip(): KazagumoPlayer { 339 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 340 | 341 | this.shoukaku.stopTrack(); 342 | 343 | return this; 344 | } 345 | 346 | /** 347 | * Seek to a position 348 | * @param position Position in seconds 349 | * @returns KazagumoPlayer 350 | */ 351 | public async seek(position: number): Promise { 352 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 353 | if (!this.queue.current) throw new KazagumoError(1, "Player has no current track in it's queue"); 354 | if (!this.queue.current.isSeekable) throw new KazagumoError(1, "The current track isn't seekable"); 355 | 356 | position = Number(position); 357 | 358 | if (isNaN(position)) throw new KazagumoError(1, 'position must be a number'); 359 | if (position < 0 || position > (this.queue.current.length ?? 0)) 360 | position = Math.max(Math.min(position, this.queue.current.length ?? 0), 0); 361 | 362 | this.queue.current.position = position; 363 | await this.shoukaku.seekTo(position); 364 | 365 | return this; 366 | } 367 | 368 | /** 369 | * Set the volume in percentage (default 100%) 370 | * @param volume Volume 371 | * @returns KazagumoPlayer 372 | */ 373 | public async setVolume(volume: number): Promise { 374 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 375 | if (isNaN(volume)) throw new KazagumoError(1, 'volume must be a number'); 376 | 377 | await this.node.rest.updatePlayer({ 378 | guildId: this.guildId, 379 | playerOptions: { 380 | volume, 381 | }, 382 | }); 383 | 384 | this.volume = volume; 385 | 386 | return this; 387 | } 388 | 389 | /** 390 | * Connect to the voice channel 391 | * @returns KazagumoPlayer 392 | */ 393 | public connect(): KazagumoPlayer { 394 | if (this.state === PlayerState.DESTROYED) throw new KazagumoError(1, 'Player is already destroyed'); 395 | if (this.state === PlayerState.CONNECTED || !!this.voiceId) 396 | throw new KazagumoError(1, 'Player is already connected'); 397 | this.state = PlayerState.CONNECTING; 398 | 399 | this.kazagumo.KazagumoOptions.send(this.guildId, { 400 | op: 4, 401 | d: { 402 | guild_id: this.guildId, 403 | channel_id: this.voiceId, 404 | self_mute: false, 405 | self_deaf: this.options.deaf, 406 | }, 407 | }); 408 | 409 | this.state = PlayerState.CONNECTED; 410 | 411 | this.emit(Events.Debug, `Player ${this.guildId} connected`); 412 | 413 | return this; 414 | } 415 | 416 | /** 417 | * Disconnect from the voice channel 418 | * @returns KazagumoPlayer 419 | */ 420 | public disconnect(): KazagumoPlayer { 421 | if (this.state === PlayerState.DISCONNECTED || !this.voiceId) 422 | throw new KazagumoError(1, 'Player is already disconnected'); 423 | this.state = PlayerState.DISCONNECTING; 424 | 425 | this.pause(true); 426 | this.kazagumo.KazagumoOptions.send(this.guildId, { 427 | op: 4, 428 | d: { 429 | guild_id: this.guildId, 430 | channel_id: null, 431 | self_mute: false, 432 | self_deaf: false, 433 | }, 434 | }); 435 | 436 | this.voiceId = null; 437 | this.state = PlayerState.DISCONNECTED; 438 | 439 | this.emit(Events.Debug, `Player disconnected; Guild id: ${this.guildId}`); 440 | 441 | return this; 442 | } 443 | 444 | /** 445 | * Destroy the player 446 | * @returns KazagumoPlayer 447 | */ 448 | async destroy(): Promise { 449 | if (this.state === PlayerState.DESTROYING || this.state === PlayerState.DESTROYED) 450 | throw new KazagumoError(1, 'Player is already destroyed'); 451 | 452 | this.disconnect(); 453 | this.state = PlayerState.DESTROYING; 454 | this.shoukaku.clean(); 455 | await this.kazagumo.shoukaku.leaveVoiceChannel(this.guildId); 456 | await this.shoukaku.destroy(); 457 | this.shoukaku.removeAllListeners(); 458 | this.kazagumo.players.delete(this.guildId); 459 | this.state = PlayerState.DESTROYED; 460 | 461 | this.emit(Events.PlayerDestroy, this); 462 | this.emit(Events.Debug, `Player destroyed; Guild id: ${this.guildId}`); 463 | 464 | return this; 465 | } 466 | 467 | private emit(event: string, ...args: any): void { 468 | this.kazagumo.emit(event, ...args); 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /src/Managers/Supports/KazagumoQueue.ts: -------------------------------------------------------------------------------- 1 | import { KazagumoTrack } from './KazagumoTrack'; 2 | import { Events, KazagumoError } from '../../Modules/Interfaces'; 3 | import { KazagumoPlayer } from '../KazagumoPlayer'; 4 | 5 | export class KazagumoQueue extends Array { 6 | constructor(private readonly kazagumoPlayer: KazagumoPlayer) { 7 | super(); 8 | } 9 | /** Get the size of queue */ 10 | public get size() { 11 | return this.length; 12 | } 13 | 14 | /** Get the size of queue including current */ 15 | public get totalSize(): number { 16 | return this.length + (this.current ? 1 : 0); 17 | } 18 | 19 | /** Check if the queue is empty or not */ 20 | public get isEmpty() { 21 | return this.length === 0; 22 | } 23 | 24 | /** Get the queue's duration */ 25 | public get durationLength() { 26 | return this.reduce((acc, cur) => acc + (cur.length || 0), 0); 27 | } 28 | 29 | /** Current playing track */ 30 | public current: KazagumoTrack | undefined | null = null; 31 | /** Previous playing tracks */ 32 | public previous: KazagumoTrack[] = []; 33 | 34 | /** 35 | * Add track(s) to the queue 36 | * @param track KazagumoTrack to add 37 | * @returns KazagumoQueue 38 | */ 39 | public add(track: KazagumoTrack | KazagumoTrack[]): KazagumoQueue { 40 | if (!Array.isArray(track)) track = [track]; 41 | 42 | if (!this.current) { 43 | if (Array.isArray(track)) this.current = track.shift(); 44 | else { 45 | this.current = track; 46 | return this; 47 | } 48 | } 49 | 50 | if (Array.isArray(track)) for (const t of track) this.push(t); 51 | else this.push(track); 52 | this.emitChanges(); 53 | return this; 54 | } 55 | 56 | /** 57 | * Remove track from the queue 58 | * @param position Position of the track 59 | * @returns KazagumoQueue 60 | */ 61 | public remove(position: number): KazagumoQueue { 62 | if (position < 0 || position >= this.length) 63 | throw new KazagumoError(1, 'Position must be between 0 and ' + (this.length - 1)); 64 | this.splice(position, 1); 65 | this.emitChanges(); 66 | return this; 67 | } 68 | 69 | /** Shuffle the queue */ 70 | public shuffle(): KazagumoQueue { 71 | for (let i = this.length - 1; i > 0; i--) { 72 | const j = Math.floor(Math.random() * (i + 1)); 73 | [this[i], this[j]] = [this[j], this[i]]; 74 | } 75 | this.emitChanges(); 76 | return this; 77 | } 78 | 79 | /** Clear the queue */ 80 | public clear(): KazagumoQueue { 81 | this.splice(0, this.length); 82 | this.emitChanges(); 83 | return this; 84 | } 85 | 86 | private emitChanges(): void { 87 | // @ts-ignore 88 | this.kazagumoPlayer.shoukaku.emit(Events.QueueUpdate, this.kazagumoPlayer, this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Managers/Supports/KazagumoTrack.ts: -------------------------------------------------------------------------------- 1 | import { Kazagumo } from '../../Kazagumo'; 2 | import { 3 | escapeRegExp, 4 | Events, 5 | KazagumoError, 6 | RawTrack, 7 | ResolveOptions, 8 | SourceIDs, 9 | SupportedSources, 10 | } from '../../Modules/Interfaces'; 11 | import { Track } from 'shoukaku'; 12 | import { KazagumoPlayer } from '../KazagumoPlayer'; 13 | 14 | export class KazagumoTrack { 15 | /** 16 | * Kazagumo Instance 17 | */ 18 | public kazagumo: Kazagumo | undefined; 19 | /** 20 | * Track Requester 21 | */ 22 | public requester: unknown | undefined; 23 | 24 | /** Track's Base64 */ 25 | public track: string; 26 | /** Track's source */ 27 | public sourceName: string; 28 | /** Track's title */ 29 | public title: string; 30 | /** Track's URI */ 31 | public uri?: string; 32 | /** Track's identifier */ 33 | public identifier: string; 34 | /** Whether the track is seekable */ 35 | public isSeekable: boolean; 36 | /** Whether the track is a stream */ 37 | public isStream: boolean; 38 | /** Track's author */ 39 | public author: string | undefined; 40 | /** Track's length */ 41 | public length: number | undefined; 42 | /** Track's position (I don't know this) */ 43 | public position: number | undefined; 44 | /** Track's thumbnail, if available */ 45 | public thumbnail: string | undefined; 46 | /** The YouTube/soundcloud URI for spotify and other unsupported source */ 47 | public realUri?: string; 48 | 49 | public resolvedBySource: boolean = false; 50 | 51 | constructor( 52 | private readonly raw: Track, 53 | requester: unknown, 54 | ) { 55 | this.kazagumo = undefined; 56 | 57 | this.track = raw.encoded; 58 | this.sourceName = raw.info.sourceName; 59 | this.title = raw.info.title; 60 | this.uri = raw.info.uri; 61 | this.identifier = raw.info.identifier; 62 | this.isSeekable = raw.info.isSeekable; 63 | this.isStream = raw.info.isStream; 64 | this.author = raw.info.author; 65 | this.length = raw.info.length; 66 | this.thumbnail = raw.info.artworkUrl; 67 | this.realUri = SupportedSources.includes(this.sourceName) ? this.uri : undefined; 68 | 69 | this.requester = requester; 70 | 71 | if (this.sourceName === 'youtube' && this.identifier) 72 | this.thumbnail = `https://img.youtube.com/vi/${this.identifier}/hqdefault.jpg`; 73 | } 74 | 75 | /** 76 | * Get json of this track 77 | * @returns {RawTrack} 78 | */ 79 | public getRaw(): RawTrack { 80 | return { 81 | track: this.track, 82 | info: { 83 | title: this.title, 84 | uri: this.uri, 85 | identifier: this.identifier, 86 | author: this.author, 87 | sourceName: this.sourceName, 88 | isSeekable: this.isSeekable, 89 | isStream: this.isStream, 90 | length: this.length, 91 | position: this.position, 92 | artworkUrl: this.thumbnail, 93 | }, 94 | _raw: this.raw, 95 | }; 96 | } 97 | 98 | /** 99 | * Set kazagumo instance 100 | * @param kazagumo Kazagumo instance 101 | * @returns KazagumoTrack 102 | */ 103 | setKazagumo(kazagumo: Kazagumo): KazagumoTrack { 104 | this.kazagumo = kazagumo; 105 | if (this.sourceName === 'youtube' && this.identifier) 106 | this.thumbnail = `https://img.youtube.com/vi/${this.identifier}/${ 107 | kazagumo.KazagumoOptions.defaultYoutubeThumbnail ?? 'hqdefault' 108 | }.jpg`; 109 | 110 | return this; 111 | } 112 | 113 | /** 114 | * Whether the track is ready to play or need to be solved 115 | */ 116 | get readyToPlay(): boolean { 117 | return ( 118 | this.kazagumo !== undefined && 119 | !!this.track && 120 | !!this.sourceName && 121 | !!this.identifier && 122 | !!this.author && 123 | !!this.length && 124 | !!this.title && 125 | !!this.uri && 126 | !!this.realUri 127 | ); 128 | } 129 | 130 | /** 131 | * Resolve the track 132 | * @param options Resolve options 133 | * @returns Promise 134 | */ 135 | public async resolve(options?: ResolveOptions): Promise { 136 | if (!this.kazagumo) throw new KazagumoError(1, 'Kazagumo is not set'); 137 | if ( 138 | this.kazagumo.KazagumoOptions.trackResolver && 139 | typeof this.kazagumo.KazagumoOptions.trackResolver === 'function' && 140 | (await this.kazagumo.KazagumoOptions.trackResolver.bind(this)(options)) 141 | ) 142 | return this; 143 | const resolveSource = this.kazagumo.KazagumoOptions?.sourceForceResolve?.includes(this.sourceName); 144 | const { forceResolve, overwrite } = options ? options : { forceResolve: false, overwrite: false }; 145 | 146 | if (!forceResolve && this.readyToPlay) return this; 147 | if (resolveSource && this.resolvedBySource) return this; 148 | if (resolveSource) { 149 | this.resolvedBySource = true; 150 | return this; 151 | } 152 | 153 | this.kazagumo.emit(Events.Debug, `Resolving ${this.sourceName} track ${this.title}; Source: ${this.sourceName}`); 154 | 155 | const result = await this.getTrack(options?.player ?? null); 156 | if (!result) throw new KazagumoError(2, 'No results found'); 157 | 158 | this.track = result.encoded; 159 | this.realUri = result.info.uri; 160 | this.length = result.info.length; 161 | 162 | if (overwrite || resolveSource) { 163 | this.title = result.info.title; 164 | this.identifier = result.info.identifier; 165 | this.isSeekable = result.info.isSeekable; 166 | this.author = result.info.author; 167 | this.length = result.info.length; 168 | this.isStream = result.info.isStream; 169 | this.uri = result.info.uri; 170 | } 171 | return this; 172 | } 173 | 174 | private async getTrack(player: KazagumoPlayer | null): Promise { 175 | if (!this.kazagumo) throw new Error('Kazagumo is not set'); 176 | 177 | const defaultSearchEngine = this.kazagumo.KazagumoOptions.defaultSearchEngine; 178 | const source = ((SourceIDs as any)[defaultSearchEngine || 'youtube'] || 'yt') + 'search:'; 179 | const query = [this.author, this.title].filter((x) => !!x).join(' - '); 180 | const node = await this.kazagumo.getLeastUsedNode(); 181 | 182 | if (!node) throw new KazagumoError(1, 'No nodes available'); 183 | 184 | const result = player 185 | ? await player.search(query, { source, requester: this.requester }) 186 | : await this.kazagumo.search(query, { engine: defaultSearchEngine, requester: this.requester }); 187 | if (!result || !result.tracks.length) throw new KazagumoError(2, 'No results found'); 188 | 189 | const rawTracks = result.tracks.map((x) => x.getRaw()._raw); 190 | 191 | if (this.author) { 192 | const author = [this.author, `${this.author} - Topic`]; 193 | const officialTrack = rawTracks.find( 194 | (track) => 195 | author.some((name) => new RegExp(`^${escapeRegExp(name)}$`, 'i').test(track.info.author)) || 196 | new RegExp(`^${escapeRegExp(this.title)}$`, 'i').test(track.info.title), 197 | ); 198 | if (officialTrack) return officialTrack; 199 | } 200 | if (this.length) { 201 | const sameDuration = rawTracks.find( 202 | (track) => 203 | track.info.length >= (this.length ? this.length : 0) - 2000 && 204 | track.info.length <= (this.length ? this.length : 0) + 2000, 205 | ); 206 | if (sameDuration) return sameDuration; 207 | } 208 | 209 | return rawTracks[0]; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Modules/Interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Kazagumo } from '../Kazagumo'; 2 | import { KazagumoPlayer, KazagumoQueue } from '../Index'; 3 | import { KazagumoTrack } from '../Managers/Supports/KazagumoTrack'; 4 | import { Constructor } from './Utils'; 5 | import { Track } from 'shoukaku'; 6 | 7 | export interface KazagumoOptions { 8 | /** Default search engine if no engine was provided. Default to youtube. If defaultSource is provided, this will be ignored */ 9 | defaultSearchEngine: SearchEngines; 10 | /** Default source if no source was provided. Default to defaultSearchEngine */ 11 | defaultSource?: string; 12 | /** Kazagumo plugins */ 13 | plugins?: KazagumoPlugin[]; 14 | /** Source that will be forced to resolve when playing it */ 15 | sourceForceResolve?: string[]; 16 | /** The track resolver. Make sure you set .track for it to work. (I'm not responsible for any error during playback if you don't set it right) */ 17 | trackResolver?: (this: KazagumoTrack, options?: ResolveOptions) => Promise; 18 | /** The default youtube thumbnail's size */ 19 | defaultYoutubeThumbnail?: YoutubeThumbnail; 20 | /** Extend some of the Structures */ 21 | extends?: { 22 | player?: Constructor; 23 | }; 24 | /** Send to guild's shard */ 25 | send: (guildId: string, payload: Payload) => void; 26 | } 27 | 28 | export type SearchEngines = 'youtube' | 'soundcloud' | 'youtube_music' | string; 29 | export type YoutubeThumbnail = 'default' | 'hqdefault' | 'mqdefault' | 'sddefault' | 'maxresdefault'; 30 | 31 | export interface Payload { 32 | /** The OP code */ 33 | op: number; 34 | d: { 35 | guild_id: string; 36 | channel_id: string | null; 37 | self_mute: boolean; 38 | self_deaf: boolean; 39 | }; 40 | } 41 | 42 | export const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 43 | 44 | export const SourceIDs = { 45 | youtube: 'yt', 46 | youtube_music: 'ytm', 47 | soundcloud: 'sc', 48 | }; 49 | 50 | export interface KazagumoPlayerOptions { 51 | guildId: string; 52 | voiceId: string; 53 | textId?: string; 54 | deaf: boolean; 55 | volume: number; 56 | /** Whether the node for searching track should be the same as the node for playing track. Default: true */ 57 | searchWithSameNode?: boolean; 58 | extends?: { 59 | queue?: Constructor; 60 | }; 61 | } 62 | 63 | export interface ResolveOptions { 64 | overwrite?: boolean; 65 | forceResolve?: boolean; 66 | player?: KazagumoPlayer; 67 | } 68 | 69 | export interface CreatePlayerOptions { 70 | /** The player's guild ID */ 71 | guildId: string; 72 | /** The player's voice ID */ 73 | voiceId: string; 74 | /** The player's text ID */ 75 | textId?: string; 76 | /** Whether the bot should deafen */ 77 | deaf?: boolean; 78 | /** Whether the bot should mute */ 79 | mute?: boolean; 80 | /** The player's guild's shardId */ 81 | shardId?: number; 82 | /** Balance the node? */ 83 | loadBalancer?: boolean; 84 | /** The player's volume */ 85 | volume?: number; 86 | /** Use specific node */ 87 | nodeName?: string; 88 | /** The player's data, usable when you extends it */ 89 | data?: unknown; 90 | } 91 | 92 | export interface RawTrack { 93 | track: string; 94 | info: { 95 | title: string; 96 | uri?: string; 97 | identifier: string; 98 | sourceName: string; 99 | isSeekable: boolean; 100 | isStream: boolean; 101 | author?: string; 102 | length?: number; 103 | position?: number; 104 | artworkUrl?: string; 105 | }; 106 | _raw: Track; 107 | } 108 | 109 | export const Events = { 110 | // Player events 111 | PlayerDestroy: 'playerDestroy', 112 | PlayerCreate: 'playerCreate', 113 | PlayerStart: 'playerStart', 114 | PlayerEnd: 'playerEnd', 115 | PlayerEmpty: 'playerEmpty', 116 | PlayerClosed: 'playerClosed', 117 | PlayerUpdate: 'playerUpdate', 118 | PlayerException: 'playerException', 119 | PlayerError: 'playerError', 120 | PlayerResumed: 'playerResumed', 121 | PlayerStuck: 'playerStuck', 122 | PlayerResolveError: 'playerResolveError', 123 | PlayerMoved: 'playerMoved', 124 | QueueUpdate: 'queueUpdate', 125 | 126 | // Kazagumo events 127 | Debug: 'debug', 128 | }; 129 | 130 | export interface PlayerMovedChannels { 131 | oldChannelId?: string | null; 132 | newChannelId?: string | null; 133 | } 134 | 135 | export type PlayerMovedState = 'UNKNOWN' | 'JOINED' | 'LEFT' | 'MOVED'; 136 | 137 | export interface KazagumoSearchOptions { 138 | requester: unknown; 139 | source?: string; 140 | engine?: SearchEngines; 141 | nodeName?: string; 142 | } 143 | 144 | export interface KazagumoSearchResult { 145 | type: SearchResultTypes; 146 | playlistName?: string; 147 | tracks: KazagumoTrack[]; 148 | } 149 | 150 | export type SearchResultTypes = 'PLAYLIST' | 'TRACK' | 'SEARCH'; 151 | 152 | export const SupportedSources = [ 153 | 'bandcamp', 154 | 'beam', 155 | 'getyarn', 156 | 'http', 157 | 'local', 158 | 'nico', 159 | 'soundcloud', 160 | 'stream', 161 | 'twitch', 162 | 'vimeo', 163 | 'youtube', 164 | 'spotify', 165 | ]; 166 | 167 | export interface PlayOptions { 168 | position?: number; 169 | endTime?: number; 170 | volume?: number; 171 | paused?: boolean; 172 | replaceCurrent?: boolean; 173 | } 174 | 175 | export enum State { 176 | CONNECTING, 177 | NEARLY, 178 | CONNECTED, 179 | RECONNECTING, 180 | DISCONNECTING, 181 | DISCONNECTED, 182 | } 183 | 184 | export enum PlayerState { 185 | CONNECTING, 186 | CONNECTED, 187 | DISCONNECTING, 188 | DISCONNECTED, 189 | DESTROYING, 190 | DESTROYED, 191 | } 192 | 193 | export class KazagumoPlugin { 194 | public load(kazagumo: Kazagumo): void { 195 | throw new KazagumoError(1, 'Plugin must implement load()'); 196 | } 197 | 198 | public unload(kazagumo: Kazagumo): void { 199 | throw new KazagumoError(1, 'Plugin must implement unload()'); 200 | } 201 | } 202 | 203 | /* tslint:disable:max-classes-per-file */ 204 | export class KazagumoError extends Error { 205 | public code: number; 206 | public message: string; 207 | 208 | public constructor(code: number, message: string) { 209 | super(message); 210 | this.code = code; 211 | this.message = message; 212 | } 213 | } 214 | 215 | export enum VoiceState { 216 | SESSION_READY, 217 | SESSION_ID_MISSING, 218 | SESSION_ENDPOINT_MISSING, 219 | SESSION_FAILED_UPDATE, 220 | } 221 | -------------------------------------------------------------------------------- /src/Modules/Plugins.ts: -------------------------------------------------------------------------------- 1 | import { KazagumoPlugin as PlayerMoved } from '../Plugins/PlayerMoved'; 2 | 3 | export default { 4 | PlayerMoved, 5 | }; 6 | -------------------------------------------------------------------------------- /src/Modules/Utils.ts: -------------------------------------------------------------------------------- 1 | import { Track } from 'shoukaku'; 2 | import { KazagumoTrack } from '../Managers/Supports/KazagumoTrack'; 3 | 4 | export class KazagumoUtils { 5 | static convertKazagumoTrackToTrack(track: KazagumoTrack | Track): Track { 6 | if ((track as Track).info) return track as Track; 7 | track = track as KazagumoTrack; 8 | return { 9 | encoded: track.track, 10 | info: { 11 | isSeekable: track.isSeekable, 12 | isStream: track.isStream, 13 | title: track.title, 14 | uri: track.uri, 15 | identifier: track.identifier, 16 | sourceName: track.sourceName, 17 | author: track.author ?? '', 18 | length: track.length ?? 0, 19 | position: track.position ?? 0, 20 | }, 21 | pluginInfo: {}, 22 | }; 23 | } 24 | } 25 | // Credit: Deivu (developer of Shoukaku) https://github.com/shipgirlproject/Shoukaku/blob/e7d94081cabbda7327dc04e467a45fcda49c24f2/src/Utils.ts#L1C1-L2C1 26 | export type Constructor = new (...args: any[]) => T; 27 | -------------------------------------------------------------------------------- /src/Plugins/PlayerMoved.ts: -------------------------------------------------------------------------------- 1 | import { Kazagumo, Events } from '../Index'; 2 | import { KazagumoPlugin as Plugin } from '../Modules/Interfaces'; 3 | 4 | export class KazagumoPlugin extends Plugin { 5 | /** 6 | * Kazagumo instance. 7 | */ 8 | public kazagumo: Kazagumo | null = null; 9 | 10 | /** 11 | * Initialize the plugin. 12 | * @param client Discord.Client 13 | */ 14 | constructor(public client: any) { 15 | super(); 16 | } 17 | 18 | /** 19 | * Load the plugin. 20 | * @param kazagumo Kazagumo 21 | */ 22 | public load(kazagumo: Kazagumo): void { 23 | this.kazagumo = kazagumo; 24 | this.client.on('voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); 25 | } 26 | 27 | /** 28 | * Unload the plugin. 29 | */ 30 | public unload(): void { 31 | this.client.removeListener('voiceStateUpdate', this.onVoiceStateUpdate.bind(this)); 32 | this.kazagumo = null; 33 | } 34 | 35 | private onVoiceStateUpdate(oldState: any, newState: any): void { 36 | if (!this.kazagumo || oldState.id !== this.client.user.id) return; 37 | 38 | const newChannelId = newState.channelID || newState.channelId; 39 | const oldChannelId = oldState.channelID || oldState.channelId; 40 | const guildId = newState.guild.id; 41 | 42 | const player = this.kazagumo.players.get(guildId); 43 | if (!player) return; 44 | 45 | let state = 'UNKNOWN'; 46 | if (!oldChannelId && newChannelId) state = 'JOINED'; 47 | else if (oldChannelId && !newChannelId) state = 'LEFT'; 48 | else if (oldChannelId && newChannelId && oldChannelId !== newChannelId) state = 'MOVED'; 49 | 50 | if (state === 'UNKNOWN') return; 51 | 52 | this.kazagumo.emit(Events.PlayerMoved, player, state, { oldChannelId, newChannelId }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | }, 12 | "rules": { 13 | "unified-signatures": false 14 | }, 15 | "include": [ 16 | "src" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "**/__tests__/*", 21 | "test", 22 | "node_modules/**", 23 | "test/**", 24 | "/node_modules/" 25 | ] 26 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"] 3 | } --------------------------------------------------------------------------------
Generated using TypeDoc
Const