Add music module and some fix and ToDos

This commit is contained in:
Ethanell 2021-11-22 00:44:41 +01:00
parent 9a7fbfa58f
commit bf177e6548
18 changed files with 1244 additions and 17 deletions

2
.gitignore vendored
View file

@ -107,7 +107,7 @@ dist
.idea
# TypeScript compiled sources
*.js
build/
# Configuration
config.json

579
package-lock.json generated
View file

@ -11,8 +11,12 @@
"dependencies": {
"@discordjs/builders": "^0.6.0",
"@discordjs/rest": "^0.1.0-canary.0",
"@discordjs/voice": "^0.7.5",
"discord-api-types": "^0.23.1",
"discord.js": "^13.1.0"
"discord.js": "^13.1.0",
"libsodium-wrappers": "^0.7.9",
"youtube-dl-exec": "^2.0.0",
"ytdl-core": "^4.9.1"
},
"devDependencies": {
"typescript": "^4.5.2"
@ -95,6 +99,59 @@
"node": ">=12"
}
},
"node_modules/@discordjs/voice": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.7.5.tgz",
"integrity": "sha512-lUk+CmIXNKslT6DkC9IF9rpsqhzlTiedauUCPBzepjd4XWxwBZiyVIzR6QpbAirxkAwCoAbbje+3Ho71PGLEAw==",
"dependencies": {
"@types/ws": "^8.2.0",
"discord-api-types": "^0.24.0",
"prism-media": "^1.3.2",
"tiny-typed-emitter": "^2.1.0",
"tslib": "^2.3.1",
"ws": "^8.2.3"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@discordjs/voice/node_modules/@types/ws": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz",
"integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@discordjs/voice/node_modules/discord-api-types": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz",
"integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==",
"engines": {
"node": ">=12"
}
},
"node_modules/@discordjs/voice/node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@sapphire/async-queue": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz",
@ -173,6 +230,27 @@
"node": ">= 0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/dargs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz",
"integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==",
"engines": {
"node": ">=8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -254,6 +332,28 @@
"node": ">=6"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -267,6 +367,25 @@
"node": ">= 6"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"engines": {
"node": ">=10.17.0"
}
},
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
@ -275,11 +394,65 @@
"node": ">=8"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unix": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-unix/-/is-unix-2.0.1.tgz",
"integrity": "sha512-RyKp5JtlRnfOvnKtfBMPLw9ocqDe1NlPQ8Bt+geVzKGfMnLGj8z/Y2HOmk/aMf47P4EbrEt9dN6YGTT1fx4mZA==",
"engines": {
"node": ">= 12"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"node_modules/libsodium": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz",
"integrity": "sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A=="
},
"node_modules/libsodium-wrappers": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz",
"integrity": "sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==",
"dependencies": {
"libsodium": "^0.7.0"
}
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"node_modules/m3u8stream": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz",
"integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==",
"dependencies": {
"miniget": "^4.0.0",
"sax": "^1.2.4"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"node_modules/mime-db": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
@ -299,10 +472,37 @@
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/miniget": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz",
"integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-fetch": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.4.tgz",
"integrity": "sha512-aD1fO+xtLiSCc9vuD+sYMxpIuQyhHscGSkBEo2o5LTV/3bTEAYvdUii29n8LlO5uLCmWdGP7uVUVXFo5SRdkLA==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@ -310,6 +510,31 @@
"node": "4.x || >=6.0.0"
}
},
"node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dependencies": {
"path-key": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ow": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
@ -329,6 +554,81 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/prism-media": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz",
"integrity": "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==",
"peerDependencies": {
"@discordjs/opus": "^0.5.0",
"ffmpeg-static": "^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
}
}
},
"node_modules/sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
},
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"engines": {
"node": ">=6"
}
},
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -390,6 +690,20 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/ws": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
@ -409,6 +723,35 @@
"optional": true
}
}
},
"node_modules/youtube-dl-exec": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/youtube-dl-exec/-/youtube-dl-exec-2.0.0.tgz",
"integrity": "sha512-wD5BSkxC1o3rzj+1ktXdWObvkorW3Zvl1E+l56JQqYFTek3d9SR7o1RbjLiQZYQpXpoCPw0zV16SNDXjhVksXQ==",
"hasInstallScript": true,
"dependencies": {
"dargs": "~7.0.0",
"execa": "~5.1.0",
"is-unix": "~2.0.1",
"mkdirp": "~1.0.4",
"node-fetch": "~2.6.5"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/ytdl-core": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.9.1.tgz",
"integrity": "sha512-6Jbp5RDhUEozlaJQAR+l8oV8AHsx3WUXxSyPxzE6wOIAaLql7Hjiy0ZM58wZoyj1YEenlEPjEqcJIjKYKxvHtQ==",
"dependencies": {
"m3u8stream": "^0.8.3",
"miniget": "^4.0.0",
"sax": "^1.1.3"
},
"engines": {
"node": ">=10"
}
}
},
"dependencies": {
@ -473,6 +816,40 @@
}
}
},
"@discordjs/voice": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.7.5.tgz",
"integrity": "sha512-lUk+CmIXNKslT6DkC9IF9rpsqhzlTiedauUCPBzepjd4XWxwBZiyVIzR6QpbAirxkAwCoAbbje+3Ho71PGLEAw==",
"requires": {
"@types/ws": "^8.2.0",
"discord-api-types": "^0.24.0",
"prism-media": "^1.3.2",
"tiny-typed-emitter": "^2.1.0",
"tslib": "^2.3.1",
"ws": "^8.2.3"
},
"dependencies": {
"@types/ws": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz",
"integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==",
"requires": {
"@types/node": "*"
}
},
"discord-api-types": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz",
"integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A=="
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
}
}
},
"@sapphire/async-queue": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz",
@ -527,6 +904,21 @@
"delayed-stream": "~1.0.0"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"dargs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz",
"integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -584,6 +976,22 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
},
"execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
"integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"requires": {
"cross-spawn": "^7.0.3",
"get-stream": "^6.0.0",
"human-signals": "^2.1.0",
"is-stream": "^2.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^4.0.1",
"onetime": "^5.1.2",
"signal-exit": "^3.0.3",
"strip-final-newline": "^2.0.0"
}
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@ -594,16 +1002,68 @@
"mime-types": "^2.1.12"
}
},
"get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
},
"human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
},
"is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="
},
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="
},
"is-unix": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-unix/-/is-unix-2.0.1.tgz",
"integrity": "sha512-RyKp5JtlRnfOvnKtfBMPLw9ocqDe1NlPQ8Bt+geVzKGfMnLGj8z/Y2HOmk/aMf47P4EbrEt9dN6YGTT1fx4mZA=="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"libsodium": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz",
"integrity": "sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A=="
},
"libsodium-wrappers": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz",
"integrity": "sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==",
"requires": {
"libsodium": "^0.7.0"
}
},
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"m3u8stream": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.4.tgz",
"integrity": "sha512-sco80Db+30RvcaIOndenX6E6oQNgTiBKeJbFPc+yDXwPQIkryfboEbCvXPlBRq3mQTCVPQO93TDVlfRwqpD35w==",
"requires": {
"miniget": "^4.0.0",
"sax": "^1.2.4"
}
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
"mime-db": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
@ -617,14 +1077,45 @@
"mime-db": "1.49.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"miniget": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.1.tgz",
"integrity": "sha512-O/DduzDR6f+oDtVype9S/Qu5hhnx73EDYGyZKwU/qN82lehFZdfhoa4DT51SpsO+8epYrB3gcRmws56ROfTIoQ=="
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"node-fetch": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.4.tgz",
"integrity": "sha512-aD1fO+xtLiSCc9vuD+sYMxpIuQyhHscGSkBEo2o5LTV/3bTEAYvdUii29n8LlO5uLCmWdGP7uVUVXFo5SRdkLA==",
"version": "2.6.6",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz",
"integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
"integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"requires": {
"path-key": "^3.0.0"
}
},
"onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"ow": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz",
@ -638,6 +1129,50 @@
"vali-date": "^1.0.0"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
},
"prism-media": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.2.tgz",
"integrity": "sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==",
"requires": {}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
},
"signal-exit": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
},
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="
},
"tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -683,11 +1218,41 @@
"webidl-conversions": "^3.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"requires": {
"isexe": "^2.0.0"
}
},
"ws": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
"requires": {}
},
"youtube-dl-exec": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/youtube-dl-exec/-/youtube-dl-exec-2.0.0.tgz",
"integrity": "sha512-wD5BSkxC1o3rzj+1ktXdWObvkorW3Zvl1E+l56JQqYFTek3d9SR7o1RbjLiQZYQpXpoCPw0zV16SNDXjhVksXQ==",
"requires": {
"dargs": "~7.0.0",
"execa": "~5.1.0",
"is-unix": "~2.0.1",
"mkdirp": "~1.0.4",
"node-fetch": "~2.6.5"
}
},
"ytdl-core": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.9.1.tgz",
"integrity": "sha512-6Jbp5RDhUEozlaJQAR+l8oV8AHsx3WUXxSyPxzE6wOIAaLql7Hjiy0ZM58wZoyj1YEenlEPjEqcJIjKYKxvHtQ==",
"requires": {
"m3u8stream": "^0.8.3",
"miniget": "^4.0.0",
"sax": "^1.1.3"
}
}
}
}

View file

@ -7,8 +7,9 @@
"test": "test"
},
"scripts": {
"run": "tsc && node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"run": " node build/index.js",
"dev": "rm -fr build && tsc && node build/index.js"
},
"repository": {
"type": "git",
@ -26,8 +27,12 @@
"dependencies": {
"@discordjs/builders": "^0.6.0",
"@discordjs/rest": "^0.1.0-canary.0",
"@discordjs/voice": "^0.7.5",
"discord-api-types": "^0.23.1",
"discord.js": "^13.1.0"
"discord.js": "^13.1.0",
"libsodium-wrappers": "^0.7.9",
"youtube-dl-exec": "^2.0.0",
"ytdl-core": "^4.9.1"
},
"devDependencies": {
"typescript": "^4.5.2"

View file

@ -1,14 +1,20 @@
import "fs";
import { Intents } from "discord.js";
import {Intents, Message} from "discord.js";
import "./lib/Modules";
import {AdministratorClient} from "./lib/AdministratorClient";
const config = require("../config.json");
const client = new AdministratorClient({ intents: [Intents.FLAGS.GUILDS] });
const client = new AdministratorClient({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_VOICE_STATES] });
client.once("ready", async () => {
client.application = await client.application?.fetch() ?? null;
if ("DEV" in process.env && process.env["DEV"] == "true") {
console.log("Dev mod enable");
await client.application?.commands.set([]);
for (const name in await client.guilds.fetch())
await (await client.guilds.fetch(name)).commands.set([]);
}
await client.modules.loadAllModules();
console.log("Started !");
});
@ -24,7 +30,20 @@ client.on("interactionCreate", async interaction => {
await command.execute(interaction);
} catch (error) {
console.error(error);
await interaction.reply({ content: "There was an error while executing this command !", ephemeral: true });
const msg = {content: "There was an error while executing this command !", ephemeral: true};
try {
await interaction.reply(msg);
} catch {
try {
await interaction.followUp(msg);
} catch {
try {
await (await interaction.fetchReply() as Message).reply(msg);
} catch {
console.warn("Cant send error message to the user :/");
}
}
}
}
});

View file

@ -20,7 +20,12 @@ export abstract class Command {
async register() {
try {
this.scope = await this.module.modules.client?.application?.commands.create(this.data);
if ("DEV" in process.env && process.env["DEV"] == "true") {
const devGuild = await this.module.modules.client?.guilds.fetch(process.env["DEVGUILD"] as any);
this.scope = await devGuild.commands.create(this.data); // ToDo: use only one call to avoid spamming the api
} else {
this.scope = await this.module.modules.client?.application?.commands.create(this.data);
}
console.log("Successfully registered commands " + this.scope?.name);
} catch (error) {
@ -30,7 +35,7 @@ export abstract class Command {
async isRegister(): Promise<boolean> {
if (this.scope)
return !! await this.module.modules.client?.application?.commands.fetch(this.scope.id);
return !! await this.module.modules.client?.application?.commands.fetch(this.scope.id); // ToDo: use only one call to avoid spamming the api
return false;
}

131
src/modules/Music/Player.ts Normal file
View file

@ -0,0 +1,131 @@
import {VoiceChannel} from "discord.js";
import {
AudioPlayer,
AudioPlayerState,
AudioPlayerStatus,
AudioResource,
createAudioPlayer,
entersState,
joinVoiceChannel,
VoiceConnection,
VoiceConnectionDisconnectReason,
VoiceConnectionState,
VoiceConnectionStatus
} from '@discordjs/voice';
import {promisify} from "util";
import {Track} from "./Track";
const wait = promisify(setTimeout);
export class Player {
readonly connexion: VoiceConnection;
readonly audio: AudioPlayer;
current: Track | null = null;
queue: Track[] = [];
readyLock: boolean = false;
queueLock: boolean = false;
constructor(voiceChanel: VoiceChannel) {
this.connexion = joinVoiceChannel({channelId: voiceChanel.id, guildId: voiceChanel.guildId, selfDeaf: true, selfMute: false, adapterCreator: voiceChanel.guild.voiceAdapterCreator as any});
this.audio = createAudioPlayer();
this.connexion.on("error", console.warn);
this.audio.on('error', (error: { resource: any; }) => (error.resource as AudioResource<Track>).metadata.onError(error as any));
this.connexion.on("stateChange", async (_: VoiceConnectionState, newState: VoiceConnectionState) => {
if (newState.status === VoiceConnectionStatus.Disconnected) {
if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
try {
await entersState(this.connexion, VoiceConnectionStatus.Connecting, 5_000);
} catch {
this.connexion.destroy();
}
} else if (this.connexion.rejoinAttempts < 5) {
await wait((this.connexion.rejoinAttempts + 1) * 5_000);
this.connexion.rejoin();
} else {
this.connexion.destroy();
}
} else if (newState.status === VoiceConnectionStatus.Destroyed) {
this.stop();
} else if (
!this.readyLock &&
(newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)
) {
this.readyLock = true;
try {
await entersState(this.connexion, VoiceConnectionStatus.Ready, 20_000);
} catch (e) {
if (this.connexion.state.status !== VoiceConnectionStatus.Destroyed) this.connexion.destroy();
} finally {
this.readyLock = false;
}
}
});
this.audio.on('stateChange', (oldState: AudioPlayerState, newState: AudioPlayerState) => {
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
(oldState.resource as AudioResource<Track>).metadata.onFinish();
void this.processQueue();
} else if (newState.status === AudioPlayerStatus.Playing) {
(newState.resource as AudioResource<Track>).metadata.onStart();
}
});
this.connexion.subscribe(this.audio);
}
public enqueue(track: Track) {
this.queue.push(track);
void this.processQueue();
}
public pause() {
this.audio.pause();
}
public resume() {
this.audio.unpause();
}
public async skip() {
this.audio.stop(true);
await this.processQueue();
}
public stop() {
this.queueLock = true;
this.queue = [];
this.audio.stop(true);
}
public disconnect() {
if (this.audio.state.status != AudioPlayerStatus.Idle)
this.stop();
this.connexion.disconnect();
}
public flush() {
this.queue = [];
}
private async processQueue(): Promise<void> {
if (this.queueLock || this.audio.state.status !== AudioPlayerStatus.Idle || this.queue.length === 0) {
return;
}
this.queueLock = true;
const nextTrack = this.queue.shift()!;
try {
const resource = await nextTrack.createAudioResource();
this.audio.play(resource);
this.queueLock = false;
this.current = nextTrack;
} catch (error) {
await nextTrack.onError(error as Error);
this.queueLock = false;
this.current = null;
return this.processQueue();
}
}
}

View file

@ -0,0 +1,79 @@
import {getInfo, videoInfo} from "ytdl-core";
import { AudioResource, createAudioResource, demuxProbe } from "@discordjs/voice";
import {exec as ytdl} from "youtube-dl-exec";
import {CommandInteraction, Message} from "discord.js";
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = async () => {};
export class Track {
public readonly info: videoInfo;
private readonly interaction: CommandInteraction;
private constructor(info: videoInfo, interaction: CommandInteraction) {
this.info = info;
this.interaction = interaction
}
private async replyInteraction(message: string) {
try {
await this.interaction.followUp(message);
} catch {
await (await this.interaction.fetchReply() as Message).reply(message);
}
}
async onStart() {
this.onStart = noop;
await this.replyInteraction("Now playing");
}
async onFinish() {
this.onStart = noop;
}
async onError(error: Error) {
this.onStart = noop;
console.error(error);
await this.replyInteraction("Error with this song, sorry :/");
}
public createAudioResource(): Promise<AudioResource<Track>> {
return new Promise((resolve, reject) => {
const process = ytdl(
this.info.videoDetails.video_url,
{
output: "-",
quiet: true,
format: 'bestaudio[ext=webm+acodec=opus+asr=48000]/bestaudio',
limitRate: '100K'
},
{ stdio: ['ignore', 'pipe', 'ignore'] },
);
if (!process.stdout) {
reject(new Error('No stdout'));
return;
}
const stream = process.stdout;
const onError = (error: Error) => {
if (!process.killed) process.kill();
stream.resume();
reject(error);
};
process
.once('spawn', () => {
demuxProbe(stream)
.then((probe: { stream: any; type: any; }) => resolve(createAudioResource(probe.stream, { metadata: this, inputType: probe.type })))
.catch(onError);
})
.catch(onError);
});
}
public static async from(url: string, interaction: CommandInteraction): Promise<Track> {
const info = await getInfo(url);
return new Track(info, interaction);
}
}

View file

@ -0,0 +1,39 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class DisconnectCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "disconnect",
description: "Stop the music"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
}
player.disconnect();
await interaction.followUp("Bot disconnected");
}
}

View file

@ -0,0 +1,43 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerPausedState, AudioPlayerStatus} from "@discordjs/voice";
export class FlushCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "flush",
description: "Flush the music queue"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
} else if (!player.queue.length) {
await interaction.editReply("Can't flush queue, there is no music left");
return;
}
player.flush();
await interaction.followUp("Queue flushed");
}
}

View file

@ -0,0 +1,30 @@
import {Module} from "../../lib/Module";
import {Modules} from "../../lib/Modules";
import {PlayCommand} from "./play";
import {Snowflake} from "discord-api-types";
import {Player} from "./Player";
import {StopCommand} from "./stop";
import {PauseCommand} from "./pause";
import {SkipCommand} from "./skip";
import {ResumeCommand} from "./resume";
import {FlushCommand} from "./flush";
import {QueueCommand} from "./queue";
import {DisconnectCommand} from "./disconnect";
export class Music extends Module {
players: Map<Snowflake, Player> = new Map<Snowflake, Player>();
constructor(modules: Modules) {
super(modules);
this.commands.push(new PlayCommand(this));
this.commands.push(new StopCommand(this));
this.commands.push(new PauseCommand(this));
this.commands.push(new ResumeCommand(this));
this.commands.push(new SkipCommand(this));
this.commands.push(new FlushCommand(this));
this.commands.push(new QueueCommand(this));
this.commands.push(new DisconnectCommand(this));
// ToDo: stop if nobody in the channel
}
}

View file

@ -0,0 +1,43 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerPausedState, AudioPlayerStatus} from "@discordjs/voice";
export class PauseCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "pause",
description: "Pause the music"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
} else if ([AudioPlayerStatus.Playing, AudioPlayerStatus.Buffering].includes(player.audio.state.status)) {
await interaction.editReply(`Can't pause, the music is ${player.audio.state.status}`);
return;
}
player.pause();
await interaction.followUp("Music paused");
}
}

72
src/modules/Music/play.ts Normal file
View file

@ -0,0 +1,72 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember, VoiceChannel} from "discord.js";
import {Music} from "./index";
import {Player} from "./Player";
import {Track} from "./Track";
import {entersState, VoiceConnectionStatus} from "@discordjs/voice";
const {Constants: { ApplicationCommandOptionTypes }} = require("discord.js");
export class PlayCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "play",
description: "Play a music",
options: [{
type: ApplicationCommandOptionTypes.STRING,
name: "music",
description: "The music to play",
required: true
}]
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
if (! interaction.member.voice.channel || ! (interaction.member.voice.channel instanceof VoiceChannel)) {
await interaction.editReply("You must be connected into a voice channel !");
return;
} else if (!interaction.member.voice.channel.joinable) {
await interaction.editReply("The bot doesn't have the permission to join this voice channel :/");
return;
}
player = new Player(interaction.member.voice.channel);
this.module.players.set(interaction.guild.id, player);
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
}
try {
await entersState(player.connexion, VoiceConnectionStatus.Ready, 20e3);
} catch (error) {
console.warn("Fail to enter state Ready !");
await interaction.followUp("Failed to join voice channel within 20 seconds, please try again later !");
return;
}
const url = interaction.options.get("music")!.value! as string;
try {
const track = await Track.from(url, interaction);
player.enqueue(track);
await interaction.followUp(`${track.info.videoDetails.title} added to queue`);
} catch (error) {
console.error(error);
await interaction.followUp("Fail to add to queue")
}
}
}

View file

@ -0,0 +1,67 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerStatus} from "@discordjs/voice";
function millisecondsToTime(milli: number): string {
const seconds = Math.floor((milli / 1000) % 60);
const minutes = Math.floor((milli / (60 * 1000)) % 60);
return ('0' + minutes).slice(-2) + ":" + ('0' + seconds).slice(-2);
}
export class QueueCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "queue",
description: "Display the current queue"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
} else if (player.audio.state.status == AudioPlayerStatus.Idle) {
await interaction.editReply("There is no queue to display");
return;
}
let queue = "";
if (player.queue.length) {
queue = player.queue.map((m, n) => `${n}. ${m.info.videoDetails.title}`).join("\n") + "\n";
}
let barr = "";
if ([AudioPlayerStatus.Playing, AudioPlayerStatus.Paused, AudioPlayerStatus.AutoPaused].includes(player.audio.state.status)) {
// @ts-ignore
const duration: number = player.current?.info.videoDetails.lengthSeconds * 1000;
// @ts-ignore
const current: number = player.audio.state.playbackDuration;
const maxSize = 35;
const progress = Math.ceil((current/duration)*maxSize);
barr = `\n${player.current?.info.videoDetails.title}\n${millisecondsToTime(current)} [${"=".repeat(progress)}${"-".repeat(maxSize-progress)}] ${millisecondsToTime(duration)}\n`
}
await interaction.followUp(`\`\`\`md\n${queue}${barr}\`\`\``);
}
}

View file

@ -0,0 +1,43 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class ResumeCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "resume",
description: "Resume the music"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
} else if ([AudioPlayerStatus.Paused, AudioPlayerStatus.AutoPaused].includes(player.audio.state.status)) {
await interaction.editReply(`Can't resume, the music is ${player.audio.state.status}`);
return;
}
player.resume();
await interaction.followUp("Music resumed");
}
}

43
src/modules/Music/skip.ts Normal file
View file

@ -0,0 +1,43 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class SkipCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "skip",
description: "Skip the music"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
} else if (player.audio.state.status == AudioPlayerStatus.Idle) {
await interaction.editReply("Can't skip, there is no music");
return;
}
await player.skip();
await interaction.followUp("Music skipped");
}
}

42
src/modules/Music/stop.ts Normal file
View file

@ -0,0 +1,42 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {Music} from "./index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class StopCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "stop",
description: "Stop the music"
};
module: Music;
constructor(module: Music) {
super(module);
this.module = module;
}
async execute(interaction: CommandInteraction) {
await interaction.deferReply();
if (!interaction.guild || ! (interaction.member instanceof GuildMember)) {
await interaction.editReply("This command is only usable in a guild :/");
return;
}
let player = this.module.players.get(interaction.guild.id);
if (!player) {
await interaction.editReply("No music currently playing !");
return;
} else if (interaction.member.voice.channelId != player.connexion.joinConfig.channelId) {
await interaction.editReply("You must be in the same voice channel !");
return;
} else if (player.audio.state.status == AudioPlayerStatus.Idle) {
await interaction.editReply("Can't stop, there is no music");
return;
}
player.stop();
await interaction.followUp("Music stopped");
}
}

View file

@ -8,6 +8,7 @@ import {
TextChannel,
VoiceChannel
} from "discord.js";
const {Constants: { ApplicationCommandOptionTypes }} = require("discord.js");

View file

@ -6,7 +6,7 @@
"lib": ["es2021"],
"module": "commonjs",
"target": "es2021",
"outDir": "build",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,