summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksa Vuckovic <aleksa@vuckovic.cc>2025-11-11 14:14:48 +0100
committerYour Name <you@example.com>2025-11-13 19:07:45 +0100
commitf10e48a8d8d0cdca589c9d73791b9a46e896425d (patch)
tree97055c143dffa352cec1a3e122a9061584a1e324
Initial commit
-rw-r--r--.env.example3
-rw-r--r--.gitignore4
-rw-r--r--index.js1
-rw-r--r--package-lock.json1125
-rw-r--r--package.json21
-rw-r--r--pajser-bot.service19
-rw-r--r--src/bot.js113
-rw-r--r--src/commands/definitions.js89
-rw-r--r--src/handlers/controlCommands.js97
-rw-r--r--src/handlers/playCommand.js271
-rw-r--r--src/handlers/queueCommand.js38
-rw-r--r--src/handlers/seekCommand.js51
-rw-r--r--src/handlers/volumeCommand.js28
-rw-r--r--src/utils/commandRegistry.js16
-rw-r--r--src/utils/helpers.js24
-rw-r--r--src/utils/player.js516
16 files changed, 2416 insertions, 0 deletions
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..6ccac30
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+DISCORD_TOKEN=your_bot_token_here
+CLIENT_ID=your_client_id_here
+COOKIES_FILE=/path/to/cookies.txt
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..728ef1b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+.env
+*.log
+cookies.txt
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..7f64e9b
--- /dev/null
+++ b/index.js
@@ -0,0 +1 @@
+require('./src/bot');
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..8c0896d
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1125 @@
+{
+ "name": "pajser",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "pajser",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "@discordjs/opus": "^0.10.0",
+ "@discordjs/voice": "^0.18.0",
+ "discord.js": "^14.24.2",
+ "dotenv": "^17.2.3",
+ "opusscript": "^0.0.8",
+ "sodium-native": "^5.0.9"
+ }
+ },
+ "node_modules/@discordjs/builders": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz",
+ "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@discordjs/formatters": "^0.6.1",
+ "@discordjs/util": "^1.1.1",
+ "@sapphire/shapeshift": "^4.0.0",
+ "discord-api-types": "^0.38.31",
+ "fast-deep-equal": "^3.1.3",
+ "ts-mixer": "^6.0.4",
+ "tslib": "^2.6.3"
+ },
+ "engines": {
+ "node": ">=16.11.0"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/builders/node_modules/discord-api-types": {
+ "version": "0.38.33",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz",
+ "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==",
+ "license": "MIT",
+ "workspaces": [
+ "scripts/actions/documentation"
+ ]
+ },
+ "node_modules/@discordjs/collection": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
+ "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.11.0"
+ }
+ },
+ "node_modules/@discordjs/formatters": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz",
+ "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "discord-api-types": "^0.38.1"
+ },
+ "engines": {
+ "node": ">=16.11.0"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/formatters/node_modules/discord-api-types": {
+ "version": "0.38.33",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz",
+ "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==",
+ "license": "MIT",
+ "workspaces": [
+ "scripts/actions/documentation"
+ ]
+ },
+ "node_modules/@discordjs/node-pre-gyp": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/@discordjs/node-pre-gyp/-/node-pre-gyp-0.4.5.tgz",
+ "integrity": "sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.7",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ }
+ },
+ "node_modules/@discordjs/opus": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@discordjs/opus/-/opus-0.10.0.tgz",
+ "integrity": "sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@discordjs/node-pre-gyp": "^0.4.5",
+ "node-addon-api": "^8.1.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/@discordjs/rest": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
+ "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@discordjs/collection": "^2.1.1",
+ "@discordjs/util": "^1.1.1",
+ "@sapphire/async-queue": "^1.5.3",
+ "@sapphire/snowflake": "^3.5.3",
+ "@vladfrangu/async_event_emitter": "^2.4.6",
+ "discord-api-types": "^0.38.16",
+ "magic-bytes.js": "^1.10.0",
+ "tslib": "^2.6.3",
+ "undici": "6.21.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
+ "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/rest/node_modules/discord-api-types": {
+ "version": "0.38.33",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz",
+ "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==",
+ "license": "MIT",
+ "workspaces": [
+ "scripts/actions/documentation"
+ ]
+ },
+ "node_modules/@discordjs/util": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
+ "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/voice": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz",
+ "integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/ws": "^8.5.12",
+ "discord-api-types": "^0.37.103",
+ "prism-media": "^1.3.5",
+ "tslib": "^2.6.3",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/ws": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
+ "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@discordjs/collection": "^2.1.0",
+ "@discordjs/rest": "^2.5.1",
+ "@discordjs/util": "^1.1.0",
+ "@sapphire/async-queue": "^1.5.2",
+ "@types/ws": "^8.5.10",
+ "@vladfrangu/async_event_emitter": "^2.2.4",
+ "discord-api-types": "^0.38.1",
+ "tslib": "^2.6.2",
+ "ws": "^8.17.0"
+ },
+ "engines": {
+ "node": ">=16.11.0"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
+ "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/@discordjs/ws/node_modules/discord-api-types": {
+ "version": "0.38.33",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz",
+ "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==",
+ "license": "MIT",
+ "workspaces": [
+ "scripts/actions/documentation"
+ ]
+ },
+ "node_modules/@sapphire/async-queue": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
+ "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=v14.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/@sapphire/shapeshift": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
+ "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=v16"
+ }
+ },
+ "node_modules/@sapphire/snowflake": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
+ "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=v14.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
+ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@vladfrangu/async_event_emitter": {
+ "version": "2.4.7",
+ "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
+ "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=v14.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "license": "ISC"
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
+ "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
+ "license": "ISC"
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/bare-addon-resolve": {
+ "version": "1.9.6",
+ "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.6.tgz",
+ "integrity": "sha512-hvOQY1zDK6u0rSr27T6QlULoVLwi8J2k8HHHJlxSfT7XQdQ/7bsS+AnjYkHtu/TkL+gm3aMXAKucJkJAbrDG/g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-module-resolve": "^1.10.0",
+ "bare-semver": "^1.0.0"
+ },
+ "peerDependencies": {
+ "bare-url": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-url": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-module-resolve": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.0.tgz",
+ "integrity": "sha512-JrzrqlC3Tds0iKRwQs8xIIJ+FRieKA9ll0jaqpotDLZtjJPVevzRoeuUYZ5GIo1t1z7/pIRdk85Q3i/2xQLfEQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-semver": "^1.0.0"
+ },
+ "peerDependencies": {
+ "bare-url": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-url": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-semver": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
+ "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "license": "MIT"
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
+ "license": "ISC"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/discord-api-types": {
+ "version": "0.37.120",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz",
+ "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==",
+ "license": "MIT"
+ },
+ "node_modules/discord.js": {
+ "version": "14.24.2",
+ "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz",
+ "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@discordjs/builders": "^1.13.0",
+ "@discordjs/collection": "1.5.3",
+ "@discordjs/formatters": "^0.6.1",
+ "@discordjs/rest": "^2.6.0",
+ "@discordjs/util": "^1.1.1",
+ "@discordjs/ws": "^1.2.3",
+ "@sapphire/snowflake": "3.5.3",
+ "discord-api-types": "^0.38.31",
+ "fast-deep-equal": "3.1.3",
+ "lodash.snakecase": "4.1.1",
+ "magic-bytes.js": "^1.10.0",
+ "tslib": "^2.6.3",
+ "undici": "6.21.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/discordjs/discord.js?sponsor"
+ }
+ },
+ "node_modules/discord.js/node_modules/discord-api-types": {
+ "version": "0.38.33",
+ "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz",
+ "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==",
+ "license": "MIT",
+ "workspaces": [
+ "scripts/actions/documentation"
+ ]
+ },
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "license": "ISC"
+ },
+ "node_modules/gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
+ "license": "ISC"
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.snakecase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+ "license": "MIT"
+ },
+ "node_modules/magic-bytes.js": {
+ "version": "1.12.1",
+ "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
+ "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
+ "license": "MIT"
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/node-addon-api": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
+ "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "deprecated": "This package is no longer supported.",
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/opusscript": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/opusscript/-/opusscript-0.0.8.tgz",
+ "integrity": "sha512-VSTi1aWFuCkRCVq+tx/BQ5q9fMnQ9pVZ3JU4UHKqTkf0ED3fKEPdr+gKAAl3IA2hj9rrP6iyq3hlcJq3HELtNQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prism-media": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
+ "integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==",
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "@discordjs/opus": ">=0.8.0 <1.0.0",
+ "ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0",
+ "node-opus": "^0.3.3",
+ "opusscript": "^0.0.8"
+ },
+ "peerDependenciesMeta": {
+ "@discordjs/opus": {
+ "optional": true
+ },
+ "ffmpeg-static": {
+ "optional": true
+ },
+ "node-opus": {
+ "optional": true
+ },
+ "opusscript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/require-addon": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
+ "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-addon-resolve": "^1.3.0"
+ },
+ "engines": {
+ "bare": ">=1.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "license": "ISC"
+ },
+ "node_modules/sodium-native": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.9.tgz",
+ "integrity": "sha512-6fpu3d6zdrRpLhuV3CDIBO5g90KkgaeR+c3xvDDz0ZnDkAlqbbPhFW7zhMJfsskfZ9SuC3SvBbqvxcECkXRyKw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-addon": "^1.1.0",
+ "which-runtime": "^1.2.1"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/ts-mixer": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
+ "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/undici": {
+ "version": "6.21.3",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
+ "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "license": "MIT"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which-runtime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
+ "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "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/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..16f5cba
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "pajser",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "dependencies": {
+ "@discordjs/opus": "^0.10.0",
+ "@discordjs/voice": "^0.18.0",
+ "discord.js": "^14.24.2",
+ "dotenv": "^17.2.3",
+ "opusscript": "^0.0.8",
+ "sodium-native": "^5.0.9"
+ }
+}
diff --git a/pajser-bot.service b/pajser-bot.service
new file mode 100644
index 0000000..fd6d1ae
--- /dev/null
+++ b/pajser-bot.service
@@ -0,0 +1,19 @@
+[Unit]
+Description=Discord Music Bot (Pajser)
+After=network.target
+
+[Service]
+Type=simple
+User=pajser
+WorkingDirectory=/home/pajser/pajser
+ExecStart=/usr/bin/node index.js
+Restart=always
+RestartSec=10
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=pajser-bot
+
+Environment=NODE_ENV=production
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/bot.js b/src/bot.js
new file mode 100644
index 0000000..cd7c2db
--- /dev/null
+++ b/src/bot.js
@@ -0,0 +1,113 @@
+const { Client, GatewayIntentBits } = require('discord.js');
+require('dotenv').config();
+
+const commands = require('./commands/definitions');
+const { registerCommands } = require('./utils/commandRegistry');
+const { handlePlay } = require('./handlers/playCommand');
+const { handlePause, handleResume, handleSkip, handleClear, handleLoop, handleQuit } = require('./handlers/controlCommands');
+const { handleQueue } = require('./handlers/queueCommand');
+const { handleVolume } = require('./handlers/volumeCommand');
+const { handleSeek } = require('./handlers/seekCommand');
+
+const client = new Client({
+ intents: [
+ GatewayIntentBits.Guilds,
+ GatewayIntentBits.GuildVoiceStates,
+ ],
+});
+
+const queues = new Map();
+
+client.once('ready', async () => {
+ console.log(`Bot ready: ${client.user.tag}`);
+ console.log(`Ready at: ${new Date().toISOString()}`);
+ await registerCommands(commands, process.env.DISCORD_TOKEN, process.env.CLIENT_ID);
+
+ setInterval(() => {
+ const activeQueues = queues.size;
+ console.log(`[HEARTBEAT] ${new Date().toISOString()} - Active queues: ${activeQueues}`);
+ }, 5 * 60 * 1000);
+});
+
+function handleError(error, context = 'Unknown') {
+ const timestamp = new Date().toISOString();
+ console.error(`[ERROR] ${timestamp} - Context: ${context}`);
+ console.error(error);
+
+ if (error.stack) {
+ console.error('Stack trace:', error.stack);
+ }
+}
+
+async function handleInteractionError(interaction, error, commandName) {
+ handleError(error, `Command: ${commandName}`);
+
+ try {
+ const reply = interaction.deferred || interaction.replied ? 'editReply' : 'reply';
+ await interaction[reply]({
+ content: 'An error occurred while executing this command!',
+ ephemeral: true
+ }).catch(() => {});
+ } catch (replyError) {
+ console.error('Failed to send error message to user:', replyError);
+ }
+}
+
+client.on('interactionCreate', async (interaction) => {
+ if (!interaction.isChatInputCommand()) return;
+
+ try {
+ switch (interaction.commandName) {
+ case 'play':
+ await handlePlay(interaction, queues);
+ break;
+ case 'pause':
+ handlePause(interaction, queues);
+ break;
+ case 'resume':
+ handleResume(interaction, queues);
+ break;
+ case 'skip':
+ handleSkip(interaction, queues);
+ break;
+ case 'clear':
+ handleClear(interaction, queues);
+ break;
+ case 'loop':
+ handleLoop(interaction, queues);
+ break;
+ case 'quit':
+ handleQuit(interaction, queues);
+ break;
+ case 'queue':
+ handleQueue(interaction, queues);
+ break;
+ case 'volume':
+ handleVolume(interaction, queues);
+ break;
+ case 'seek':
+ handleSeek(interaction, queues);
+ break;
+ }
+ } catch (error) {
+ await handleInteractionError(interaction, error, interaction.commandName);
+ }
+});
+
+process.on('unhandledRejection', (error) => {
+ handleError(error, 'Unhandled Promise Rejection');
+});
+
+process.on('uncaughtException', (error) => {
+ handleError(error, 'Uncaught Exception');
+});
+
+client.on('error', (error) => {
+ handleError(error, 'Discord Client Error');
+});
+
+client.on('warn', (info) => {
+ console.warn(`[WARN] ${new Date().toISOString()} - ${info}`);
+});
+
+client.login(process.env.DISCORD_TOKEN); \ No newline at end of file
diff --git a/src/commands/definitions.js b/src/commands/definitions.js
new file mode 100644
index 0000000..626fcc6
--- /dev/null
+++ b/src/commands/definitions.js
@@ -0,0 +1,89 @@
+const { SlashCommandBuilder } = require('discord.js');
+
+module.exports = [
+ new SlashCommandBuilder()
+ .setName('play')
+ .setDescription('Play music from YouTube')
+ .addStringOption(option =>
+ option.setName('query')
+ .setDescription('YouTube URL or search query')
+ .setRequired(true)
+ ),
+ new SlashCommandBuilder()
+ .setName('pause')
+ .setDescription('Pause the current song'),
+ new SlashCommandBuilder()
+ .setName('resume')
+ .setDescription('Resume the paused song'),
+ new SlashCommandBuilder()
+ .setName('skip')
+ .setDescription('Skip the current song'),
+ new SlashCommandBuilder()
+ .setName('clear')
+ .setDescription('Clear the entire queue'),
+ new SlashCommandBuilder()
+ .setName('loop')
+ .setDescription('Toggle loop mode')
+ .addStringOption(option =>
+ option.setName('mode')
+ .setDescription('Loop mode')
+ .setRequired(false)
+ .addChoices(
+ { name: 'Off', value: 'off' },
+ { name: 'Song', value: 'song' },
+ { name: 'Queue', value: 'queue' }
+ )
+ ),
+ new SlashCommandBuilder()
+ .setName('quit')
+ .setDescription('Leave the voice channel'),
+ new SlashCommandBuilder()
+ .setName('queue')
+ .setDescription('Show the music queue'),
+ new SlashCommandBuilder()
+ .setName('volume')
+ .setDescription('Adjust volume')
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('set')
+ .setDescription('Set volume to a specific value')
+ .addIntegerOption(option =>
+ option.setName('value')
+ .setDescription('Volume (0-100)')
+ .setRequired(true)
+ .setMinValue(0)
+ .setMaxValue(100)
+ )
+ )
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('inc')
+ .setDescription('Increase volume')
+ .addIntegerOption(option =>
+ option.setName('amount')
+ .setDescription('Amount to increase (default: 10)')
+ .setMinValue(1)
+ .setMaxValue(100)
+ )
+ )
+ .addSubcommand(subcommand =>
+ subcommand
+ .setName('dec')
+ .setDescription('Decrease volume')
+ .addIntegerOption(option =>
+ option.setName('amount')
+ .setDescription('Amount to decrease (default: 10)')
+ .setMinValue(1)
+ .setMaxValue(100)
+ )
+ ),
+ new SlashCommandBuilder()
+ .setName('seek')
+ .setDescription('Skip forward or backward in the current song')
+ .addIntegerOption(option =>
+ option.setName('seconds')
+ .setDescription('Seconds to skip (use negative to go back)')
+ .setRequired(true)
+ ),
+
+];
diff --git a/src/handlers/controlCommands.js b/src/handlers/controlCommands.js
new file mode 100644
index 0000000..14c73f7
--- /dev/null
+++ b/src/handlers/controlCommands.js
@@ -0,0 +1,97 @@
+const { playSong, safeCleanup, clearSongBuffer } = require('../utils/player');
+const { getQueueOrReply } = require('../utils/helpers');
+
+function handlePause(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Nothing is playing!');
+ if (!queue) return;
+
+ queue.player.pause();
+ interaction.reply('Paused!');
+}
+
+function handleResume(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Nothing is paused!');
+ if (!queue) return;
+
+ queue.player.unpause();
+ interaction.reply('Resumed!');
+}
+
+function handleSkip(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Nothing to skip!');
+ if (!queue || queue.songs.length === 0) {
+ if (queue) interaction.reply('Nothing to skip!');
+ return;
+ }
+
+ const playerState = queue.player.state.status;
+ console.log(`[SKIP] Guild: ${interaction.guild.id}, Skipping: ${queue.songs[0]?.title}, Player state: ${playerState}, Queue length: ${queue.songs.length}`);
+
+ clearSongBuffer(queue.songs[0], 'skipped');
+ safeCleanup(queue, 'Skip');
+
+ queue.player.stop();
+ interaction.reply(`Skipped! (${queue.songs.length - 1} songs remaining)`);
+}
+
+function handleClear(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Queue is already empty!');
+ if (!queue || queue.songs.length === 0) {
+ if (queue) interaction.reply('Queue is already empty!');
+ return;
+ }
+
+ const currentSong = queue.songs[0];
+ const clearedCount = queue.songs.length - 1;
+
+ for (let i = 1; i < queue.songs.length; i++) {
+ clearSongBuffer(queue.songs[i], 'queue cleared');
+ }
+
+ queue.songs = [currentSong];
+ console.log(`[QUEUE] Cleared ${clearedCount} songs from queue`);
+ interaction.reply(`Cleared queue! Only current song remains: ${currentSong.title}`);
+}
+
+function handleLoop(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues);
+ if (!queue) return;
+
+ const mode = interaction.options.getString('mode');
+
+ if (mode) {
+ queue.loopMode = mode;
+ } else {
+ const modes = ['off', 'song', 'queue'];
+ const currentIndex = modes.indexOf(queue.loopMode || 'off');
+ queue.loopMode = modes[(currentIndex + 1) % modes.length];
+ }
+
+ const modeEmojis = {
+ off: 'Loop disabled',
+ song: 'Looping current song',
+ queue: 'Looping queue'
+ };
+
+ interaction.reply(modeEmojis[queue.loopMode]);
+}
+
+function handleQuit(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues);
+ if (!queue) return;
+
+ try { queue.player.stop(); } catch (e) {}
+ safeCleanup(queue, 'Quit');
+ queue.connection.destroy();
+ queues.delete(interaction.guild.id);
+ interaction.reply('Left voice channel!');
+}
+
+module.exports = {
+ handlePause,
+ handleResume,
+ handleSkip,
+ handleClear,
+ handleLoop,
+ handleQuit,
+};
diff --git a/src/handlers/playCommand.js b/src/handlers/playCommand.js
new file mode 100644
index 0000000..5551d28
--- /dev/null
+++ b/src/handlers/playCommand.js
@@ -0,0 +1,271 @@
+const { EmbedBuilder } = require('discord.js');
+const {
+ createAudioPlayer,
+ joinVoiceChannel,
+ AudioPlayerStatus,
+ VoiceConnectionStatus,
+ entersState,
+} = require('@discordjs/voice');
+const { getVideoInfo, playSong, preloadSong, formatDuration, safeCleanup, clearSongBuffer } = require('../utils/player');
+const { requireVoiceChannel } = require('../utils/helpers');
+
+function createSongsFromVideos(videos, requestedBy) {
+ return videos.map(v => ({
+ title: v.title,
+ url: v.url,
+ duration: v.duration,
+ thumbnail: v.thumbnail,
+ requestedBy: requestedBy,
+ }));
+}
+
+function onPlayerIdle(interaction, queue) {
+ console.log(`[PLAYER IDLE] Guild: ${interaction.guild.id}, Loop mode: ${queue.loopMode}, Is seeking: ${queue.isSeeking || false}`);
+
+ safeCleanup(queue, 'Player Idle');
+
+ if (queue.isSeeking) {
+ console.log(`[IDLE SKIP] Skipping queue shift because seek is in progress`);
+ return;
+ }
+
+ if (queue.loopMode === 'song') {
+ if (queue.songs[0]) {
+ queue.songs[0].retryCount = 0;
+ }
+ playSong(interaction.guild.id, queue);
+ } else if (queue.loopMode === 'queue') {
+ const finishedSong = queue.songs.shift();
+ queue.songs.push(finishedSong);
+ if (queue.songs.length > 0) {
+ playSong(interaction.guild.id, queue);
+ }
+ } else {
+ const finishedSong = queue.songs.shift();
+ clearSongBuffer(finishedSong, 'finished playing');
+ if (queue.songs.length > 0) {
+ playSong(interaction.guild.id, queue);
+ } else {
+ console.log(`[QUEUE EMPTY] Guild: ${interaction.guild.id}`);
+ }
+ }
+}
+
+function onPlayerError(error, interaction, queue) {
+ console.error('Audio player error:', error);
+ safeCleanup(queue, 'Player Error');
+
+ if (queue.isSeeking) {
+ console.log(`[ERROR DURING SEEK] Not shifting queue, seek in progress`);
+ queue.isSeeking = false;
+ return;
+ }
+
+ queue.songs.shift();
+ if (queue.songs.length > 0) {
+ playSong(interaction.guild.id, queue);
+ }
+}
+
+function onPlayerAutoPaused(player) {
+ console.warn('Player auto-paused, attempting to resume...');
+ try {
+ player.unpause();
+ } catch (err) {
+ console.error('Failed to unpause:', err);
+ }
+}
+
+function onConnectionError(error, queue) {
+ console.error('Voice connection error:', error);
+ safeCleanup(queue, 'Connection Error');
+}
+
+async function onConnectionDisconnected(connection, queues, guildId) {
+ try {
+ await Promise.race([
+ entersState(connection, VoiceConnectionStatus.Signalling, 5000),
+ entersState(connection, VoiceConnectionStatus.Connecting, 5000),
+ ]);
+ } catch (error) {
+ connection.destroy();
+ queues.delete(guildId);
+ }
+}
+
+function onConnectionDestroyed(queues, guildId) {
+ queues.delete(guildId);
+}
+
+function setupPlayerEventListeners(player, interaction, queue) {
+ player.on(AudioPlayerStatus.Idle, () => onPlayerIdle(interaction, queue));
+ player.on('error', (error) => onPlayerError(error, interaction, queue));
+ player.on(AudioPlayerStatus.AutoPaused, () => onPlayerAutoPaused(player));
+}
+
+function setupConnectionEventListeners(connection, queues, guildId, queue) {
+ connection.on('error', (error) => onConnectionError(error, queue));
+ connection.on(VoiceConnectionStatus.Disconnected, () =>
+ onConnectionDisconnected(connection, queues, guildId)
+ );
+ connection.on(VoiceConnectionStatus.Destroyed, () =>
+ onConnectionDestroyed(queues, guildId)
+ );
+}
+
+async function createVoiceConnection(voiceChannel, interaction) {
+ const connection = joinVoiceChannel({
+ channelId: voiceChannel.id,
+ guildId: interaction.guild.id,
+ adapterCreator: voiceChannel.guild.voiceAdapterCreator,
+ });
+
+ try {
+ await entersState(connection, VoiceConnectionStatus.Ready, 30000);
+ return connection;
+ } catch (error) {
+ console.error('Failed to join voice channel:', error);
+ connection.destroy();
+ throw error;
+ }
+}
+
+async function initializeQueue(voiceChannel, interaction, queues, songs) {
+ const player = createAudioPlayer();
+ let connection;
+
+ try {
+ connection = await createVoiceConnection(voiceChannel, interaction);
+ } catch (error) {
+ return null;
+ }
+
+ connection.subscribe(player);
+
+ const queue = {
+ voiceChannel,
+ connection,
+ player,
+ songs: songs,
+ volume: 50,
+ isPlaying: false,
+ loopMode: 'off',
+ };
+
+ queues.set(interaction.guild.id, queue);
+
+ setupPlayerEventListeners(player, interaction, queue);
+ setupConnectionEventListeners(connection, queues, interaction.guild.id, queue);
+
+ return queue;
+}
+
+function addSongsToQueue(queue, songs, interaction) {
+ const oldLength = queue.songs.length;
+ const wasIdle = queue.player.state.status === AudioPlayerStatus.Idle;
+
+ songs.forEach(song => queue.songs.push(song));
+ const newLength = queue.songs.length;
+
+ console.log(`[QUEUE ADD] Guild: ${interaction.guild.id}, Added ${songs.length} songs, Queue: ${oldLength} -> ${newLength}, Player state: ${queue.player.state.status}, Was idle: ${wasIdle}`);
+
+ if (wasIdle && oldLength === 0 && queue.songs.length > 0) {
+ if (queue.isSeeking) {
+ console.log(`[AUTO-PLAY BLOCKED] Seek in progress, not starting playback`);
+ } else {
+ console.log(`[AUTO-PLAY] Starting playback, songs in queue: ${queue.songs.length}`);
+ playSong(interaction.guild.id, queue);
+ }
+ }
+
+ if (queue.songs.length >= 2) {
+ const nextSong = queue.songs[1];
+ if (!nextSong.audioBuffer && !nextSong.isPreloading) {
+ console.log(`[PRELOAD TRIGGER] Preloading next song: ${nextSong.title}`);
+ preloadSong(nextSong).catch(() => {});
+ }
+ }
+
+ return wasIdle;
+}
+
+function createNewPlaybackEmbed(firstSong, isPlaylist, songs) {
+ const embed = new EmbedBuilder()
+ .setColor(0x00ff00)
+ .setTitle(isPlaylist ? 'Playlist Added' : 'Now Playing')
+ .setDescription(`**${firstSong.title}**`)
+ .addFields(
+ { name: 'Duration', value: formatDuration(firstSong.duration), inline: true },
+ { name: 'Requested by', value: firstSong.requestedBy, inline: true }
+ )
+ .setThumbnail(firstSong.thumbnail);
+
+ if (isPlaylist) {
+ embed.addFields({ name: 'Playlist', value: `${songs.length} songs added to queue` });
+ }
+
+ return embed;
+}
+
+function createAddToQueueEmbed(firstSong, isPlaylist, songs, queuePosition) {
+ const embed = new EmbedBuilder()
+ .setColor(0x00ff00)
+ .setTitle(isPlaylist ? 'Playlist Added to Queue' : 'Added to Queue')
+ .setDescription(`**${firstSong.title}**`)
+ .addFields(
+ { name: 'Position', value: `${queuePosition}`, inline: true },
+ { name: 'Requested by', value: firstSong.requestedBy, inline: true }
+ )
+ .setThumbnail(firstSong.thumbnail);
+
+ if (isPlaylist) {
+ embed.addFields({ name: 'Playlist', value: `${songs.length} songs added to queue` });
+ }
+
+ return embed;
+}
+
+async function handlePlay(interaction, queues) {
+ await interaction.deferReply();
+
+ const query = interaction.options.getString('query');
+ const voiceChannel = requireVoiceChannel(interaction);
+ if (!voiceChannel) return;
+
+ const videoInfo = await getVideoInfo(query);
+ if (!videoInfo) {
+ return interaction.editReply('Could not find that video!');
+ }
+
+ const isPlaylist = videoInfo.isPlaylist;
+ const videos = videoInfo.videos;
+
+ if (!videos || videos.length === 0) {
+ return interaction.editReply('Could not find any videos!');
+ }
+
+ const songs = createSongsFromVideos(videos, interaction.user.tag);
+ const firstSong = songs[0];
+
+ let queue = queues.get(interaction.guild.id);
+
+ if (!queue) {
+ queue = await initializeQueue(voiceChannel, interaction, queues, songs);
+
+ if (!queue) {
+ return interaction.editReply('Failed to join voice channel! Try again.');
+ }
+
+ const embed = createNewPlaybackEmbed(firstSong, isPlaylist, songs);
+ interaction.editReply({ embeds: [embed] });
+ playSong(interaction.guild.id, queue);
+ } else {
+ const queuePosition = queue.songs.length - songs.length + 1;
+ addSongsToQueue(queue, songs, interaction);
+
+ const embed = createAddToQueueEmbed(firstSong, isPlaylist, songs, queuePosition);
+ interaction.editReply({ embeds: [embed] });
+ }
+}
+
+module.exports = { handlePlay };
diff --git a/src/handlers/queueCommand.js b/src/handlers/queueCommand.js
new file mode 100644
index 0000000..e0fbee1
--- /dev/null
+++ b/src/handlers/queueCommand.js
@@ -0,0 +1,38 @@
+const { EmbedBuilder } = require('discord.js');
+const { formatDuration } = require('../utils/player');
+const { getQueueOrReply } = require('../utils/helpers');
+
+function handleQueue(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Queue is empty!');
+ if (!queue || queue.songs.length === 0) {
+ if (queue) interaction.reply('Queue is empty!');
+ return;
+ }
+
+ const nowPlaying = queue.songs[0];
+ const upcoming = queue.songs.slice(1, 10);
+
+ const embed = new EmbedBuilder()
+ .setColor(0x0099ff)
+ .setTitle('Queue')
+ .setDescription(
+ `**Now Playing:**\n${nowPlaying.title}\n` +
+ `Duration: ${formatDuration(nowPlaying.duration)} | Requested by: ${nowPlaying.requestedBy}`
+ )
+ .setThumbnail(nowPlaying.thumbnail);
+
+ if (upcoming.length > 0) {
+ const upcomingText = upcoming
+ .map((song, i) => `${i + 1}. ${song.title} - ${formatDuration(song.duration)}`)
+ .join('\n');
+ embed.addFields({ name: 'Up Next', value: upcomingText });
+ }
+
+ if (queue.songs.length > 10) {
+ embed.setFooter({ text: `...and ${queue.songs.length - 10} more` });
+ }
+
+ interaction.reply({ embeds: [embed] });
+}
+
+module.exports = { handleQueue };
diff --git a/src/handlers/seekCommand.js b/src/handlers/seekCommand.js
new file mode 100644
index 0000000..e48f603
--- /dev/null
+++ b/src/handlers/seekCommand.js
@@ -0,0 +1,51 @@
+const { EmbedBuilder } = require('discord.js');
+const { playSong, getCurrentProgress, formatDuration, safeCleanup } = require('../utils/player');
+const { getQueueOrReply } = require('../utils/helpers');
+
+function handleSeek(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Nothing is playing!');
+ if (!queue || queue.songs.length === 0) {
+ if (queue) interaction.reply('Nothing is playing!');
+ return;
+ }
+
+ if (queue.isSeeking) {
+ return interaction.reply('Already seeking, please wait!');
+ }
+
+ const seconds = interaction.options.getInteger('seconds');
+ const progress = getCurrentProgress(queue);
+
+ if (!progress) {
+ return interaction.reply('Cannot seek right now!');
+ }
+
+ let newPosition = progress.elapsed + seconds;
+ const song = queue.songs[0];
+
+ if (newPosition < 0) {
+ newPosition = 0;
+ }
+
+ if (newPosition >= song.duration) {
+ queue.player.stop();
+ return interaction.reply('Skipped to next song!');
+ }
+
+ queue.isSeeking = true;
+ safeCleanup(queue, 'Seek');
+
+ queue.player.stop();
+
+ console.log(`[SEEK] Guild: ${interaction.guild.id}, From: ${progress.elapsed}s, To: ${newPosition}s`);
+
+ playSong(interaction.guild.id, queue, newPosition);
+
+ const embed = new EmbedBuilder()
+ .setColor(0x00ff00)
+ .setDescription(`⏩ Seeking to ${formatDuration(newPosition)} / ${formatDuration(song.duration)}`);
+
+ interaction.reply({ embeds: [embed] });
+}
+
+module.exports = { handleSeek };
diff --git a/src/handlers/volumeCommand.js b/src/handlers/volumeCommand.js
new file mode 100644
index 0000000..dbd99de
--- /dev/null
+++ b/src/handlers/volumeCommand.js
@@ -0,0 +1,28 @@
+const { setQueueVolume } = require('../utils/player');
+const { getQueueOrReply } = require('../utils/helpers');
+
+function handleVolume(interaction, queues) {
+ const queue = getQueueOrReply(interaction, queues, 'Nothing is playing!');
+ if (!queue) return;
+
+ const subcommand = interaction.options.getSubcommand();
+
+ if (subcommand === 'set') {
+ const value = interaction.options.getInteger('value');
+ if (value < 0 || value > 100) {
+ return interaction.reply('Volume must be between 0 and 100!');
+ }
+ setQueueVolume(queue, value);
+ interaction.reply(`Volume set to ${value}%`);
+ } else if (subcommand === 'inc') {
+ const amount = interaction.options.getInteger('amount') || 10;
+ const newVolume = setQueueVolume(queue, queue.volume + amount);
+ interaction.reply(`Volume increased to ${newVolume}%`);
+ } else if (subcommand === 'dec') {
+ const amount = interaction.options.getInteger('amount') || 10;
+ const newVolume = setQueueVolume(queue, queue.volume - amount);
+ interaction.reply(`Volume decreased to ${newVolume}%`);
+ }
+}
+
+module.exports = { handleVolume };
diff --git a/src/utils/commandRegistry.js b/src/utils/commandRegistry.js
new file mode 100644
index 0000000..918e7b3
--- /dev/null
+++ b/src/utils/commandRegistry.js
@@ -0,0 +1,16 @@
+const { REST, Routes } = require('discord.js');
+
+async function registerCommands(commands, token, clientId) {
+ const rest = new REST({ version: '10' }).setToken(token);
+
+ try {
+ await rest.put(
+ Routes.applicationCommands(clientId),
+ { body: commands.map(cmd => cmd.toJSON()) }
+ );
+ } catch (error) {
+ console.error('Error registering commands:', error);
+ }
+}
+
+module.exports = { registerCommands };
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
new file mode 100644
index 0000000..f18408f
--- /dev/null
+++ b/src/utils/helpers.js
@@ -0,0 +1,24 @@
+function getQueueOrReply(interaction, queues, message = 'Not in a voice channel!') {
+ const queue = queues.get(interaction.guild.id);
+
+ if (!queue) {
+ interaction.reply(message);
+ return null;
+ }
+
+ return queue;
+}
+
+function requireVoiceChannel(interaction) {
+ if (!interaction.member?.voice?.channel) {
+ interaction.editReply('You need to be in a voice channel!');
+ return null;
+ }
+
+ return interaction.member.voice.channel;
+}
+
+module.exports = {
+ getQueueOrReply,
+ requireVoiceChannel,
+};
diff --git a/src/utils/player.js b/src/utils/player.js
new file mode 100644
index 0000000..44510d1
--- /dev/null
+++ b/src/utils/player.js
@@ -0,0 +1,516 @@
+const { spawn } = require('child_process');
+const { createAudioResource, StreamType } = require('@discordjs/voice');
+
+const COOKIES_FILE = process.env.COOKIES_FILE || 'cookies.txt';
+
+class Mutex {
+ constructor() {
+ this.locked = false;
+ this.queue = [];
+ }
+
+ async lock() {
+ if (!this.locked) {
+ this.locked = true;
+ return Promise.resolve();
+ }
+ return new Promise(resolve => this.queue.push(resolve));
+ }
+
+ unlock() {
+ if (this.queue.length > 0) {
+ this.queue.shift()();
+ } else {
+ this.locked = false;
+ }
+ }
+}
+
+const guildLocks = new Map();
+
+async function downloadWithYtDlp(song, options = {}) {
+ const { logPrefix = 'DOWNLOAD' } = options;
+
+ const ytdlp = spawn('yt-dlp', [
+ '--cookies', COOKIES_FILE,
+ '--format', 'bestaudio/best',
+ '--output', '-',
+ song.url,
+ ]);
+
+ const chunks = [];
+ let downloadComplete = false;
+
+ ytdlp.stdout.on('data', (chunk) => chunks.push(chunk));
+
+ ytdlp.stderr.on('data', (data) => {
+ console.error('[yt-dlp stderr]:', data.toString().trim());
+ });
+
+ ytdlp.on('error', (err) => {
+ console.error('yt-dlp process error:', err);
+ });
+
+ await new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ if (!downloadComplete) {
+ ytdlp.kill('SIGKILL');
+ reject(new Error('Download timeout after 60s'));
+ }
+ }, 60000);
+
+ ytdlp.on('exit', (code) => {
+ clearTimeout(timeout);
+ downloadComplete = true;
+
+ if (code !== 0) {
+ console.error(`yt-dlp exited with code ${code}`);
+ return reject(new Error(`yt-dlp failed with code ${code}`));
+ }
+
+ song.audioBuffer = Buffer.concat(chunks);
+ console.log(`[${logPrefix} COMPLETE] ${song.title}: ${(song.audioBuffer.length / 1024 / 1024).toFixed(2)} MB`);
+ resolve();
+ });
+ });
+}
+
+async function preloadSong(song) {
+ if (song.audioBuffer || song.isPreloading) return;
+
+ song.isPreloading = true;
+ console.log(`[PRELOAD] Starting background download: ${song.title}`);
+
+ try {
+ await downloadWithYtDlp(song, { logPrefix: 'PRELOAD' });
+ } catch (error) {
+ console.log(`[PRELOAD FAILED] ${song.title}: ${error.message}`);
+ } finally {
+ song.isPreloading = false;
+ }
+}
+
+async function getVideoInfo(query) {
+ return new Promise((resolve, reject) => {
+ const isUrl = query.startsWith('http://') || query.startsWith('https://');
+ const isPlaylist = isUrl && query.includes('list=');
+
+ const args = [
+ '--cookies', COOKIES_FILE,
+ '--dump-json',
+ ];
+
+ if (isPlaylist) {
+ args.push('--flat-playlist', '--yes-playlist');
+ } else {
+ args.push('--no-playlist');
+ }
+
+ if (!isUrl) {
+ args.push(`ytsearch1:${query}`);
+ } else {
+ args.push(query);
+ }
+
+ const ytdlp = spawn('yt-dlp', args);
+ let output = '';
+ let error = '';
+
+ ytdlp.stdout.on('data', (data) => {
+ output += data.toString();
+ });
+
+ ytdlp.stderr.on('data', (data) => {
+ error += data.toString();
+ });
+
+ ytdlp.on('close', (code) => {
+ if (code !== 0) {
+ console.error('yt-dlp error:', error);
+ return resolve(null);
+ }
+
+ try {
+ const lines = output.trim().split('\n').filter(l => l.trim());
+
+ if (isPlaylist && lines.length > 1) {
+ const videos = lines.map(line => {
+ try {
+ const info = JSON.parse(line);
+ return {
+ title: info.title,
+ url: info.url || `https://www.youtube.com/watch?v=${info.id}`,
+ duration: info.duration || 0,
+ thumbnail: info.thumbnail || info.thumbnails?.[0]?.url,
+ };
+ } catch (e) {
+ return null;
+ }
+ }).filter(v => v !== null);
+
+ return resolve({ isPlaylist: true, videos });
+ } else {
+ const info = JSON.parse(lines[0]);
+ resolve({
+ isPlaylist: false,
+ videos: [{
+ title: info.title,
+ url: info.webpage_url || info.url,
+ duration: info.duration || 0,
+ thumbnail: info.thumbnail,
+ }]
+ });
+ }
+ } catch (err) {
+ console.error('Parse error:', err);
+ resolve(null);
+ }
+ });
+ });
+}
+
+async function downloadAudio(song) {
+ if (song.audioBuffer) {
+ console.log(`[USING CACHE] Audio already downloaded for: ${song.title}`);
+ return;
+ }
+
+ console.log(`[DOWNLOAD] Downloading full audio for: ${song.title}`);
+ await downloadWithYtDlp(song, { logPrefix: 'DOWNLOAD' });
+}
+
+function createFFmpegProcess(seekSeconds) {
+ const ffmpegArgs = [
+ '-i', 'pipe:0',
+ '-analyzeduration', '0',
+ '-loglevel', 'error',
+ ];
+
+ if (seekSeconds > 0) {
+ ffmpegArgs.push('-ss', seekSeconds.toString());
+ }
+
+ ffmpegArgs.push(
+ '-f', 's16le',
+ '-ar', '48000',
+ '-ac', '2',
+ 'pipe:1'
+ );
+
+ return spawn('ffmpeg', ffmpegArgs);
+}
+
+function setupFFmpegMonitoring(ffmpeg, song, guildId, queue, seekSeconds, onRetry) {
+ let processesKilled = false;
+ let streamStarted = false;
+ let ffmpegErrors = '';
+ let lastDataTime = Date.now();
+
+ const cleanup = () => {
+ if (!processesKilled) {
+ processesKilled = true;
+ try {
+ ffmpeg.kill('SIGKILL');
+ } catch (e) {}
+ }
+ };
+
+ const handleRetry = () => {
+ if (queue.isSeeking) {
+ console.log(`[RETRY DURING SEEK] Resetting seek flag`);
+ queue.isSeeking = false;
+ }
+
+ if (song.retryCount < 2) {
+ song.retryCount++;
+ console.log(`[RETRY] Attempting retry ${song.retryCount}/2 for: ${song.title}`);
+ onRetry(guildId, queue, seekSeconds);
+ } else {
+ console.error(`[SKIP] Max retries reached for: ${song.title}`);
+ delete song.audioBuffer;
+ queue.songs.shift();
+ if (queue.songs.length > 0) {
+ onRetry(guildId, queue);
+ }
+ }
+ };
+
+ const startTimeout = setTimeout(() => {
+ if (!streamStarted && !processesKilled) {
+ console.error(`[TIMEOUT] FFmpeg failed to start in 10s: ${song.title}`);
+ cleanup();
+ handleRetry();
+ }
+ }, 10000);
+
+ const watchdogInterval = setInterval(() => {
+ const timeSinceLastData = Date.now() - lastDataTime;
+ if (timeSinceLastData > 10000 && !processesKilled && streamStarted) {
+ console.error(`[WATCHDOG] No data for 10s, killing ffmpeg for: ${song.title}`);
+ cleanup();
+ clearTimeout(startTimeout);
+ clearInterval(watchdogInterval);
+ handleRetry();
+ }
+ }, 5000);
+
+ ffmpeg.stderr.on('data', (data) => {
+ ffmpegErrors += data.toString();
+ console.error('[ffmpeg stderr]:', data.toString().trim());
+ });
+
+ ffmpeg.stdout.on('data', () => {
+ lastDataTime = Date.now();
+ if (!streamStarted) {
+ streamStarted = true;
+ clearTimeout(startTimeout);
+ console.log(`[STREAM STARTED] Guild: ${guildId}, Song: ${song.title}`);
+ }
+ });
+
+ ffmpeg.on('exit', (code, signal) => {
+ clearTimeout(startTimeout);
+ clearInterval(watchdogInterval);
+ if (code !== 0 && code !== null && !processesKilled) {
+ console.error(`ffmpeg exited with code ${code}, signal ${signal}`);
+ console.error('ffmpeg errors:', ffmpegErrors);
+ }
+ });
+
+ ffmpeg.on('error', (err) => {
+ console.error('ffmpeg process error:', err);
+ clearTimeout(startTimeout);
+ clearInterval(watchdogInterval);
+ cleanup();
+ });
+
+ ffmpeg.stdin.on('error', (err) => {
+ if (err.code !== 'EPIPE') {
+ console.error('ffmpeg stdin error:', err);
+ clearTimeout(startTimeout);
+ clearInterval(watchdogInterval);
+ cleanup();
+ }
+ });
+
+ ffmpeg.stdout.on('error', (err) => {
+ if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
+ console.error('ffmpeg stdout error:', err);
+ clearTimeout(startTimeout);
+ clearInterval(watchdogInterval);
+ cleanup();
+ }
+ });
+
+ return {
+ cleanup: () => {
+ clearTimeout(startTimeout);
+ clearInterval(watchdogInterval);
+ cleanup();
+ }
+ };
+}
+
+function setupResourceHandlers(resource, guildId, queue, cleanupHandlers, onRetry) {
+ if (resource.playStream && typeof resource.playStream.on === 'function') {
+ resource.playStream.on('error', (err) => {
+ console.error('Resource playStream error:', err);
+ cleanupHandlers.cleanup();
+
+ if (queue.isSeeking) {
+ console.log(`[PLAYSTREAM ERROR DURING SEEK] Resetting seek flag`);
+ queue.isSeeking = false;
+ return;
+ }
+
+ queue.songs.shift();
+ if (queue.songs.length > 0) {
+ onRetry(guildId, queue);
+ }
+ });
+
+ resource.playStream.on('close', () => {
+ if (queue.isPlaying) {
+ console.warn('Stream closed unexpectedly');
+ }
+ cleanupHandlers.cleanup();
+ });
+ } else {
+ console.warn('playStream not available on resource');
+ }
+}
+
+function setResourceVolume(resource, queue) {
+ try {
+ if (resource.volume) {
+ resource.volume.setVolume(queue.volume / 100);
+ } else {
+ console.warn('Volume control not available on resource');
+ }
+ } catch (err) {
+ console.error('Failed to set volume:', err);
+ }
+}
+
+function handlePlaybackComplete(guildId, queue, cleanupHandlers) {
+ queue.seekOffset = 0;
+ queue.isPlaying = true;
+
+ if (queue.isSeeking) {
+ queue.isSeeking = false;
+ console.log(`[SEEK COMPLETE] Guild: ${guildId}, Seek flag cleared`);
+ }
+
+ queue.cleanup = cleanupHandlers.cleanup;
+ console.log(`[PLAY SUCCESS] Guild: ${guildId}, Now playing: ${queue.songs[0].title}`);
+
+ if (queue.songs.length > 1) {
+ const nextSong = queue.songs[1];
+ preloadSong(nextSong).catch(() => {});
+ }
+}
+
+async function playSong(guildId, queue, seekSeconds = 0) {
+ if (!guildLocks.has(guildId)) {
+ guildLocks.set(guildId, new Mutex());
+ }
+ const lock = guildLocks.get(guildId);
+
+ await lock.lock();
+ try {
+ return await _playSongInternal(guildId, queue, seekSeconds);
+ } finally {
+ lock.unlock();
+ }
+}
+
+async function _playSongInternal(guildId, queue, seekSeconds = 0) {
+ const song = queue.songs[0];
+ if (!song) return;
+
+ if (!song.retryCount) {
+ song.retryCount = 0;
+ }
+
+ console.log(`[PLAY START] Guild: ${guildId}, Song: ${song.title}, URL: ${song.url}, Seek: ${seekSeconds}s, Retry: ${song.retryCount}`);
+
+ try {
+ await downloadAudio(song);
+
+ const ffmpeg = createFFmpegProcess(seekSeconds);
+ const cleanupHandlers = setupFFmpegMonitoring(ffmpeg, song, guildId, queue, seekSeconds, playSong);
+
+ ffmpeg.stdin.write(song.audioBuffer);
+ ffmpeg.stdin.end();
+
+ const resource = createAudioResource(ffmpeg.stdout, {
+ inputType: StreamType.Raw,
+ inlineVolume: true,
+ });
+
+ setupResourceHandlers(resource, guildId, queue, cleanupHandlers, playSong);
+ setResourceVolume(resource, queue);
+
+ queue.resource = resource;
+ queue.seekOffset = seekSeconds;
+ queue.player.play(resource);
+
+ handlePlaybackComplete(guildId, queue, cleanupHandlers);
+
+ } catch (error) {
+ console.error('Play error:', error);
+ if (queue.cleanup) {
+ try {
+ queue.cleanup();
+ } catch (e) {
+ console.error('Cleanup error:', e);
+ }
+ }
+
+ if (queue.isSeeking) {
+ console.log(`[CATCH ERROR DURING SEEK] Resetting seek flag`);
+ queue.isSeeking = false;
+ return;
+ }
+
+ queue.songs.shift();
+ if (queue.songs.length > 0) {
+ playSong(guildId, queue);
+ }
+ }
+}
+
+function formatDuration(seconds) {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+
+ if (hours > 0) {
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
+}
+
+function safeCleanup(queue, context = 'Unknown') {
+ if (queue.cleanup) {
+ try {
+ queue.cleanup();
+ queue.cleanup = null;
+ } catch (e) {
+ console.error(`Cleanup error (${context}):`, e);
+ }
+ }
+}
+
+function clearSongBuffer(song, reason = 'cleanup') {
+ if (song && song.audioBuffer) {
+ delete song.audioBuffer;
+ console.log(`[MEMORY] Cleared buffer for: ${song.title} (${reason})`);
+ }
+}
+
+function setQueueVolume(queue, volume) {
+ if (!queue) return false;
+
+ const clampedVolume = Math.max(0, Math.min(100, volume));
+ queue.volume = clampedVolume;
+
+ if (queue.resource?.volume) {
+ queue.resource.volume.setVolume(clampedVolume / 100);
+ }
+
+ return clampedVolume;
+}
+
+function getCurrentProgress(queue) {
+ if (!queue || !queue.songs[0]) {
+ return null;
+ }
+
+ let elapsed = 0;
+
+ if (queue.resource && queue.resource.playbackDuration !== undefined) {
+ elapsed = Math.floor(queue.resource.playbackDuration / 1000) + (queue.seekOffset || 0);
+ } else {
+ return null;
+ }
+
+ const duration = queue.songs[0].duration;
+
+ return {
+ elapsed,
+ duration,
+ percentage: duration > 0 ? Math.min(100, (elapsed / duration) * 100) : 0,
+ };
+}
+
+module.exports = {
+ getVideoInfo,
+ playSong,
+ preloadSong,
+ formatDuration,
+ getCurrentProgress,
+ safeCleanup,
+ clearSongBuffer,
+ setQueueVolume,
+};