diff --git a/config.js b/config.js index 071f67f1..4227e91f 100644 --- a/config.js +++ b/config.js @@ -160,7 +160,7 @@ const config = { command: { disableCommand: [], // Disabled commands, all enabled by default adminCommand: ['blacklist', 'language', 'server', 'status', 'volume-default'], // Admin commands, only Admin role user can use - djCommand: ['dj', 'filter'], // DJ commands, only DJ role user can use + djCommand: ['dj', 'filter', 'stop', 'pause', 'resume', 'skip', 'clear', 'leave', 'shuffle', 'loop', 'volume'], // DJ commands, only DJ role user can use // Supported commands: 'skip', 'seek', 'pause' // When a command name is listed here, only the requester of the currently playing song may use it. // Admins can always bypass this restriction regardless. @@ -177,6 +177,11 @@ const config = { enabled : false // Enable/disable persistent queue storage }, + // Playlist settings + playlist: { + enabled : true // Enable/disable playlist feature + }, + // SQLite database settings database: { path : './data/database.db' // Path to SQLite database file diff --git a/package-lock.json b/package-lock.json index 410b848c..664ecd68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "bcryptjs": "^3.0.3", - "better-sqlite3": "^13.0.2", + "better-sqlite3": "^13.0.3", "cookie": "^2.0.1", "discord.js": "~14.27.0", "dotenv": "^17.4.2", @@ -25,12 +25,12 @@ "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.6", - "@types/node": "^26.1.2", - "eslint": "^10.8.0", - "globals": "^17.9.0", - "tsx": "^4.23.5", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", + "globals": "^17.11.0", + "tsx": "^4.23.12", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0" + "typescript-eslint": "^8.67.0" }, "engines": { "node": ">=22.23.2" @@ -941,9 +941,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -994,17 +994,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1017,7 +1017,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1033,16 +1033,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1058,14 +1058,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1080,14 +1080,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1098,9 +1098,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -1115,15 +1115,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1140,9 +1140,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -1154,16 +1154,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1182,16 +1182,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1206,13 +1206,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1306,10 +1306,9 @@ } }, "node_modules/better-sqlite3": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.2.tgz", - "integrity": "sha512-jW6oufeDhXZaiX9Lw5A+oerVClx4iFrI6uDj1zu7SqUAjak9vbJvA0NEcKLNxHiQHb6kYCoFzzXYV0YOauhV3g==", - "hasInstallScript": true, + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz", + "integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==", "license": "MIT", "dependencies": { "node-addon-api": "^8.0.0" @@ -1676,9 +1675,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -2094,9 +2093,9 @@ } }, "node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -2939,9 +2938,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.23.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz", - "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3016,16 +3015,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", - "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.66.0", - "@typescript-eslint/parser": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0" + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index cf627ef7..9055fb2d 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,9 @@ "url": "https://github.com/hmes98318/Music-Disc/issues" }, "homepage": "https://github.com/hmes98318/Music-Disc#readme", - "overrides": { - "body-parser": "^2.2.3", - "brace-expansion": "^5.0.8" - }, "dependencies": { "bcryptjs": "^3.0.3", - "better-sqlite3": "^13.0.2", + "better-sqlite3": "^13.0.3", "cookie": "^2.0.1", "discord.js": "~14.27.0", "dotenv": "^17.4.2", @@ -48,12 +44,12 @@ "@eslint/js": "^10.0.1", "@types/better-sqlite3": "^9.6.0", "@types/express": "^5.0.6", - "@types/node": "^26.1.2", - "eslint": "^10.8.0", - "globals": "^17.9.0", - "tsx": "^4.23.5", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", + "globals": "^17.11.0", + "tsx": "^4.23.12", "typescript": "~6.0.3", - "typescript-eslint": "^8.66.0" + "typescript-eslint": "^8.67.0" }, "engines": { "node": ">=22.23.2" diff --git a/src/@types/index.ts b/src/@types/index.ts index 88f7f871..9f07dbac 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -114,12 +114,17 @@ export type Config = { bot: BotConfig; nodeList: NodeOptions[]; spotify: SpotifyConfig; - blacklist: string[], + blacklist: string[]; webDashboard: WebDashboardConfig; localNode: LocalNodeConfig; command: CommandConfig; database: DatabaseConfig; queuePersistence: QueuePersistenceConfig; + playlist: PlaylistConfig; +}; + +export type PlaylistConfig = { + enabled: boolean; }; export type BotConfig = { diff --git a/src/App.ts b/src/App.ts index 18fe02f3..33f758a1 100644 --- a/src/App.ts +++ b/src/App.ts @@ -134,7 +134,12 @@ class App { * @private */ #setShutdownSignalHandlers(): void { + let isShuttingDown = false; + const shutdown = async (signal: string) => { + if (isShuttingDown) return; + isShuttingDown = true; + this.bot.logger.log( this.bot.shardId, `Received ${signal}. Closing server gracefully...`); const timeout = setTimeout(() => { @@ -143,29 +148,33 @@ class App { }, 30 * 1000); try { - // Stop periodic saves and save all active queues before shutdown + // Stop periodic saves and save all active queues in parallel before shutdown if (this.bot.config.queuePersistence.enabled && this.#client.queuePersistence) { this.bot.logger.log( this.bot.shardId, 'Saving active queues before shutdown...'); this.#client.queuePersistence.stopAllPeriodicSaves(); - for (const player of this.#client.lavashark.players.values()) { - await this.#client.queuePersistence.saveQueue(player); - } + const players = Array.from(this.#client.lavashark?.players?.values() ?? []); + await Promise.allSettled( + players.map(player => this.#client.queuePersistence!.saveQueue(player)) + ); } // Close the lavashark players connections this.bot.logger.log( this.bot.shardId, 'Closing voice channel connection...'); + const activePlayers = Array.from(this.#client.lavashark?.players?.values() ?? []); await Promise.allSettled( - Array.from(this.#client.lavashark.players.values()).map(player => player.destroy()) + activePlayers.map(player => player.destroy()) ); - await new Promise(resolve => setTimeout(resolve, 2000)); this.bot.logger.log( this.bot.shardId, 'Voice channel connection closed.'); // Close the lavashark nodes connections this.bot.logger.log( this.bot.shardId, 'Closing lavashark nodes...'); - for (const node of this.#client.lavashark.nodes) { - node.disconnect(); + if (this.#client.lavashark?.nodes) { + for (const node of this.#client.lavashark.nodes) { + try { + node.disconnect(); + } catch (_) {} + } } - await new Promise(resolve => setTimeout(resolve, 2000)); this.bot.logger.log( this.bot.shardId, 'Lavashark nodes closed.'); // Close discord.js connection @@ -227,6 +236,10 @@ const main = async () => { main(); -process.on('unhandledRejection', (error) => { - console.error('Unhandled promise rejection:', error); +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Promise Rejection at:', promise, 'reason:', reason); +}); + +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception thrown:', error); }); diff --git a/src/commands/BlacklistCommand.ts b/src/commands/BlacklistCommand.ts index 19a85c7b..747faca3 100644 --- a/src/commands/BlacklistCommand.ts +++ b/src/commands/BlacklistCommand.ts @@ -24,30 +24,32 @@ export class BlacklistCommand extends BaseCommand { /** * Build blacklist command metadata and slash command options */ - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { aliases: [], category: CommandCategory.UTILITY, - description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION'), + description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION', { lng }), name: 'blacklist', options: [ this.createUserSubcommand( 'add', 'commands:CONFIG_BLACKLIST_OPTION_ADD', + lng, ), this.createUserSubcommand( 'remove', 'commands:CONFIG_BLACKLIST_OPTION_REMOVE', + lng, ), { - description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST'), + description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST', { lng }), name: 'list', type: ApplicationCommandOptionType.Subcommand, }, ], sendTyping: true, showHelp: true, - usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE'), + usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE', { lng }), voiceChannel: false, }; } @@ -99,13 +101,14 @@ export class BlacklistCommand extends BaseCommand { private createUserSubcommand( name: BlacklistMutationAction, descriptionKey: string, + lng?: string ): ApplicationCommandSubCommandData { return { - description: i18next.t(descriptionKey), + description: i18next.t(descriptionKey, { lng }), name, options: [ { - description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER'), + description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER', { lng }), name: 'user', required: true, type: ApplicationCommandOptionType.User, diff --git a/src/commands/ClearCommand.ts b/src/commands/ClearCommand.ts index c48ea4a4..376aa800 100644 --- a/src/commands/ClearCommand.ts +++ b/src/commands/ClearCommand.ts @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ClearCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'clear', aliases: ['cls'], - description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_CLEAR_USAGE'), + description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_CLEAR_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -48,7 +48,7 @@ export class ClearCommand extends BaseCommand { } else { await context.replySuccess(bot, isRadioPlaying - ? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS', { defaultValue: '🧹 Cleared queue and stopped radio playback.' }) + ? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS') : context.t('commands:MESSAGE_CLEAR_SUCCESS')); } } diff --git a/src/commands/DashboardCommand.ts b/src/commands/DashboardCommand.ts index 7ac4cec5..c2ace976 100644 --- a/src/commands/DashboardCommand.ts +++ b/src/commands/DashboardCommand.ts @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class DashboardCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'dashboard', - aliases: ['d', 'console'], - description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE'), + aliases: ['db'], + description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/DjCommand.ts b/src/commands/DjCommand.ts index ca9d33bf..a52f82e8 100644 --- a/src/commands/DjCommand.ts +++ b/src/commands/DjCommand.ts @@ -12,12 +12,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class DjCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'dj', aliases: ['adddj'], - description: i18next.t('commands:CONFIG_DJ_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_DJ_USAGE'), + description: i18next.t('commands:CONFIG_DJ_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_DJ_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -25,7 +25,7 @@ export class DjCommand extends BaseCommand { options: [ { name: 'user', - description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION', { lng }), type: ApplicationCommandOptionType.User, required: false } diff --git a/src/commands/FilterCommand.ts b/src/commands/FilterCommand.ts index b1af98f8..d2d24152 100644 --- a/src/commands/FilterCommand.ts +++ b/src/commands/FilterCommand.ts @@ -18,12 +18,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class FilterCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'filter', aliases: ['eq', 'equalizer'], - description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_FILTER_USAGE'), + description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_FILTER_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -31,7 +31,7 @@ export class FilterCommand extends BaseCommand { options: [ { name: 'filter', - description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION', { lng }), type: 3, required: true, choices: [ diff --git a/src/commands/HelpCommand.ts b/src/commands/HelpCommand.ts index e56929a4..4032a194 100644 --- a/src/commands/HelpCommand.ts +++ b/src/commands/HelpCommand.ts @@ -17,12 +17,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class HelpCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'help', aliases: ['h'], - description: i18next.t('commands:CONFIG_HELP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_HELP_USAGE'), + description: i18next.t('commands:CONFIG_HELP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_HELP_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -30,7 +30,7 @@ export class HelpCommand extends BaseCommand { options: [ { name: 'command', - description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION', { lng }), type: 3, required: false } @@ -59,15 +59,14 @@ export class HelpCommand extends BaseCommand { * @private */ async #showCommandList(bot: Bot, client: Client, context: CommandContext): Promise { - const title = client.user?.username; - const commands = client.commands.getHelpCommands(bot); + const commands = client.commands.getHelpCommands(bot, context.language); const musicCommands = commands.filter(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); return metadata.category === CommandCategory.MUSIC; }); const utilityCommands = commands.filter(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); return metadata.category === CommandCategory.UTILITY; }); @@ -76,7 +75,7 @@ export class HelpCommand extends BaseCommand { .setCustomId(SelectButtonId.HelpMusic) .setPlaceholder(context.t('commands:HELP_SELECT_MUSIC_PLACEHOLDER')) .setOptions(musicCommands.map(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE'); return { label: metadata.name, @@ -89,7 +88,7 @@ export class HelpCommand extends BaseCommand { .setCustomId(SelectButtonId.HelpUtility) .setPlaceholder(context.t('commands:HELP_SELECT_UTILITY_PLACEHOLDER')) .setOptions(utilityCommands.map(cmd => { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE'); return { label: metadata.name, @@ -120,12 +119,12 @@ export class HelpCommand extends BaseCommand { const selectedCommand = client.commands.get(i.values[0]); if (!selectedCommand) return; - const metadata = selectedCommand.getMetadata(bot); + const metadata = selectedCommand.getMetadata(bot, context.language); const usage = `${metadata.description}\n\`\`\`${bot.config.bot.prefix}${metadata.usage}\`\`\``; await i.deferUpdate(); await msg.edit({ - embeds: [embeds.help(bot, title!, usage, context.language)], + embeds: [embeds.help(bot, metadata.name, usage, context.language)], components: [], allowedMentions: { repliedUser: false } }).catch(() => bot.logger.discord( bot.shardId, 'Failed to edit deleted message.')); @@ -150,11 +149,11 @@ export class HelpCommand extends BaseCommand { */ async #showCommandHelp(bot: Bot, client: Client, context: CommandContext, commandName: string): Promise { const prefix = bot.config.bot.prefix; - const commands = client.commands.getHelpCommands(bot); + const commands = client.commands.getHelpCommands(bot, context.language); let found = false; for (const cmd of commands) { - const metadata = cmd.getMetadata(bot); + const metadata = cmd.getMetadata(bot, context.language); if (commandName === metadata.name || (metadata.aliases && metadata.aliases.includes(commandName))) { const description = `${metadata.description}\n\`\`\`${prefix}${metadata.usage}\`\`\``; diff --git a/src/commands/JoinCommand.ts b/src/commands/JoinCommand.ts index 8ee56e04..86e8ac2b 100644 --- a/src/commands/JoinCommand.ts +++ b/src/commands/JoinCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class JoinCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'join', - aliases: ['add', 'summon'], - description: i18next.t('commands:CONFIG_JOIN_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_JOIN_USAGE'), + aliases: ['j', 'connect'], + description: i18next.t('commands:CONFIG_JOIN_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_JOIN_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -62,6 +62,7 @@ export class JoinCommand extends BaseCommand { } catch (error) { bot.logger.error( bot.shardId, 'Error joining channel: ' + error); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); return; } diff --git a/src/commands/LanguageCommand.ts b/src/commands/LanguageCommand.ts index f8bc5ad7..2f0f68cb 100644 --- a/src/commands/LanguageCommand.ts +++ b/src/commands/LanguageCommand.ts @@ -11,7 +11,7 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class LanguageCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { const choices = bot.lang.languages.slice(0, 25).map(lang => ({ name: getLanguageDisplayName(lang), value: lang @@ -20,16 +20,16 @@ export class LanguageCommand extends BaseCommand { return { name: 'language', aliases: ['lang', 'locale'], - description: i18next.t('commands:CONFIG_LANG_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LANG_USAGE'), + description: i18next.t('commands:CONFIG_LANG_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LANG_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, sendTyping: false, options: [ { - name: 'locale', - description: i18next.t('commands:CONFIG_LANG_OPTION_DESCRIPTION'), + name: 'language', + description: i18next.t('commands:CONFIG_LANG_OPTION_DESCRIPTION', { lng }), type: 3, required: false, choices @@ -40,7 +40,7 @@ export class LanguageCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { const locale = context.isInteraction() - ? context.getStringOption('locale') + ? (context.getStringOption('language') || context.getStringOption('locale')) : context.args.join(' '); if (!locale) { diff --git a/src/commands/LeaveCommand.ts b/src/commands/LeaveCommand.ts index f06d5a99..209d4171 100644 --- a/src/commands/LeaveCommand.ts +++ b/src/commands/LeaveCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class LeaveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'leave', aliases: [], - description: i18next.t('commands:CONFIG_LEAVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LEAVE_USAGE'), + description: i18next.t('commands:CONFIG_LEAVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LEAVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/LoopCommand.ts b/src/commands/LoopCommand.ts index 54887ad0..05af6c94 100644 --- a/src/commands/LoopCommand.ts +++ b/src/commands/LoopCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class LoopCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'loop', aliases: ['lp'], - description: i18next.t('commands:CONFIG_LOOP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_LOOP_USAGE'), + description: i18next.t('commands:CONFIG_LOOP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_LOOP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,7 +23,7 @@ export class LoopCommand extends BaseCommand { options: [ { name: 'mode', - description: i18next.t('commands:CONFIG_LOOP_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_LOOP_OPTION_DESCRIPTION', { lng }), type: 3, required: true, choices: [ diff --git a/src/commands/MoveCommand.ts b/src/commands/MoveCommand.ts index a63155f2..4a2f7288 100644 --- a/src/commands/MoveCommand.ts +++ b/src/commands/MoveCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class MoveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'move', aliases: ['mv', 'swap', 'change'], - description: i18next.t('commands:CONFIG_MOVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_MOVE_USAGE'), + description: i18next.t('commands:CONFIG_MOVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_MOVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -22,14 +22,14 @@ export class MoveCommand extends BaseCommand { options: [ { name: 'moveindex1', - description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION', { lng }), type: 4, required: true, min_value: 1 }, { name: 'moveindex2', - description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION_2'), + description: i18next.t('commands:CONFIG_MOVE_OPTION_DESCRIPTION_2', { lng }), type: 4, required: true, min_value: 1 diff --git a/src/commands/NodeStatusCommand.ts b/src/commands/NodeStatusCommand.ts index 397272f0..db0b014a 100644 --- a/src/commands/NodeStatusCommand.ts +++ b/src/commands/NodeStatusCommand.ts @@ -1,3 +1,4 @@ +import { ActionRowBuilder, StringSelectMenuBuilder } from 'discord.js'; import i18next from 'i18next'; import { NodeState } from 'lavashark'; @@ -11,12 +12,17 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NodeStatusCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { + const choices = (bot.config.nodeList || []).slice(0, 25).map(node => ({ + name: node.id, + value: node.id + })); + return { name: 'nodestatus', aliases: ['node', 'nodes', 'nodesstatus'], - description: i18next.t('commands:CONFIG_NODE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_NODE_USAGE'), + description: i18next.t('commands:CONFIG_NODE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_NODE_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -24,9 +30,10 @@ export class NodeStatusCommand extends BaseCommand { options: [ { name: 'nodename', - description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_NODE_OPTION_DESCRIPTION', { lng }), type: 3, - required: false + required: false, + choices: choices.length > 0 ? choices : undefined } ] }; @@ -72,8 +79,32 @@ export class NodeStatusCommand extends BaseCommand { bot.logger.log( bot.shardId, 'nodesStatus: ' + JSON.stringify(nodesStatus)); + const components = []; + if (nodes.length > 0) { + const selectOptions = nodes.slice(0, 25).map((node, i) => { + const ping = pingList[i]; + const isConnected = ping !== -1; + return { + label: node.identifier, + value: node.identifier, + description: isConnected + ? `${context.t('embeds:NODE_STATUS_PING')}: ${ping}ms` + : context.t('embeds:NODE_DISCONNECTED').replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(context.t('commands:CONFIG_NODE_OPTION_DESCRIPTION')) + .addOptions(selectOptions); + + components.push(new ActionRowBuilder().addComponents(selectMenu)); + } + await context.reply({ embeds: [embeds.nodesStatus(bot, unhealthValue, nodesStatus, context.language)], + components, allowedMentions: { repliedUser: false } }); } @@ -108,8 +139,32 @@ export class NodeStatusCommand extends BaseCommand { bot.logger.log( bot.shardId, 'nodeStats: ' + JSON.stringify(nodeStats)); bot.logger.log( bot.shardId, 'nodePing: ' + nodePing + 'ms'); + const components = []; + if (nodes.length > 0) { + const selectOptions = nodes.slice(0, 25).map(n => { + const isConnected = n.state === NodeState.CONNECTED; + return { + label: n.identifier, + value: n.identifier, + default: n.identifier === nodeName, + description: isConnected + ? `${context.t('embeds:NODE_STATUS_PING')}: ${nodePing}ms` + : context.t('embeds:NODE_DISCONNECTED').replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(context.t('commands:CONFIG_NODE_OPTION_DESCRIPTION')) + .addOptions(selectOptions); + + components.push(new ActionRowBuilder().addComponents(selectMenu)); + } + await context.reply({ embeds: [embeds.nodeStatus(bot, nodeName, nodeInfo, nodeStats, nodePing, context.language)], + components, allowedMentions: { repliedUser: false } }); return; @@ -128,3 +183,4 @@ export class NodeStatusCommand extends BaseCommand { }); } } + diff --git a/src/commands/NowPlayingCommand.ts b/src/commands/NowPlayingCommand.ts index 66d63367..6139edb9 100644 --- a/src/commands/NowPlayingCommand.ts +++ b/src/commands/NowPlayingCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class NowPlayingCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'nowplaying', aliases: ['np', 'save'], - description: i18next.t('commands:CONFIG_NOW_PLAYING_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_NOW_PLAYING_USAGE'), + description: i18next.t('commands:CONFIG_NOW_PLAYING_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_NOW_PLAYING_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: false, showHelp: true, diff --git a/src/commands/PauseCommand.ts b/src/commands/PauseCommand.ts index 5f1546dd..7417881b 100644 --- a/src/commands/PauseCommand.ts +++ b/src/commands/PauseCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Pause command - Pauses music playback */ export class PauseCommand extends BaseCommand { - getMetadata(_bot: Bot): CommandMetadata { + getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'pause', aliases: [], - description: i18next.t('commands:CONFIG_PAUSE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PAUSE_USAGE'), + description: i18next.t('commands:CONFIG_PAUSE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PAUSE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/PingCommand.ts b/src/commands/PingCommand.ts index 2ec80b7c..789f4c54 100644 --- a/src/commands/PingCommand.ts +++ b/src/commands/PingCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Ping command - Shows bot latency */ export class PingCommand extends BaseCommand { - getMetadata(_bot: Bot): CommandMetadata { + getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'ping', aliases: [], - description: i18next.t('commands:CONFIG_PING_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PING_USAGE'), + description: i18next.t('commands:CONFIG_PING_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PING_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -28,7 +28,7 @@ export class PingCommand extends BaseCommand { } protected async run(bot: Bot, client: Client, context: CommandContext): Promise { - const botPing = `${Date.now() - context.createdTimestamp}ms`; + const botPing = `${Math.abs(Date.now() - context.createdTimestamp)}ms`; const apiPing = client.ws.ping.toString(); await context.react('👍'); diff --git a/src/commands/PlayCommand.ts b/src/commands/PlayCommand.ts index e7f2967e..2ef5e103 100644 --- a/src/commands/PlayCommand.ts +++ b/src/commands/PlayCommand.ts @@ -1,9 +1,11 @@ import i18next from 'i18next'; +import { ConnectionState } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType } from '../@types/index.js'; import { embeds } from '../embeds/index.js'; import { isUserInBlacklist } from '../utils/functions/isUserInBlacklist.js'; +import { cleanSearchQuery } from '../utils/functions/cleanSearchQuery.js'; import { DJManager } from '../lib/DjManager.js'; import { QueueLimitManager } from '../lib/QueueLimitManager.js'; @@ -14,12 +16,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class PlayCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'play', aliases: ['p'], - description: i18next.t('commands:CONFIG_PLAY_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAY_USAGE'), + description: i18next.t('commands:CONFIG_PLAY_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAY_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -27,7 +29,7 @@ export class PlayCommand extends BaseCommand { options: [ { name: 'play', - description: i18next.t('commands:CONFIG_PLAY_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAY_OPTION_DESCRIPTION', { lng }), type: 3, required: true } @@ -37,15 +39,24 @@ export class PlayCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { // Get search query - const str = context.isMessage() + const rawStr = context.isMessage() ? context.args.join(' ') : context.getStringOption('play'); + const str = cleanSearchQuery(rawStr || ''); + if (!str) { await context.replyEphemeralError(bot, context.t('commands:MESSAGE_PLAY_ARGS_ERROR')); return; } + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str); + if (isM3uUrl || existingPlaylist?.isM3u) { + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); + return; + } + // Search for tracks let res; try { @@ -110,20 +121,28 @@ export class PlayCommand extends BaseCommand { } /** - * Create and initialize player + * Create and initialize player (or reuse existing player) * @private */ async #createPlayer(bot: Bot, client: Client, context: CommandContext): Promise { + const guildId = String(context.guild?.id); const voiceChannelId = context.isMessage() ? String(context.getMessage().member?.voice.channelId) : String(context.getInteraction().guild!.members.cache.get(context.user.id)?.voice.channelId); - const player = client.lavashark.createPlayer({ - guildId: String(context.guild?.id), - voiceChannelId: voiceChannelId, - textChannelId: context.channel!.id, - selfDeaf: true - }); + let player = client.lavashark.getPlayer(guildId); + + if (!player) { + player = client.lavashark.createPlayer({ + guildId: guildId, + voiceChannelId: voiceChannelId, + textChannelId: context.channel!.id, + selfDeaf: true + }); + } else { + player.voiceChannelId = voiceChannelId; + player.textChannelId = context.channel!.id; + } if (!player.setting) { player.setting = { @@ -134,14 +153,17 @@ export class PlayCommand extends BaseCommand { } const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - - try { - await player.connect(); - player.metadata = metadata; - } catch (error) { - bot.logger.error( bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - return null; + player.metadata = metadata; + + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + } catch (error) { + bot.logger.error( bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return null; + } } try { @@ -258,3 +280,4 @@ export class PlayCommand extends BaseCommand { } } } + diff --git a/src/commands/PlayFirstCommand.ts b/src/commands/PlayFirstCommand.ts index 2b3cb14d..2d49e826 100644 --- a/src/commands/PlayFirstCommand.ts +++ b/src/commands/PlayFirstCommand.ts @@ -1,9 +1,11 @@ import i18next from 'i18next'; +import { ConnectionState, RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType } from '../@types/index.js'; import { embeds } from '../embeds/index.js'; import { isUserInBlacklist } from '../utils/functions/isUserInBlacklist.js'; +import { cleanSearchQuery } from '../utils/functions/cleanSearchQuery.js'; import { DJManager } from '../lib/DjManager.js'; import { QueueLimitManager } from '../lib/QueueLimitManager.js'; @@ -14,12 +16,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class PlayFirstCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'playfirst', aliases: ['pf', 'pp', 'prioritizePlay'], - description: i18next.t('commands:CONFIG_PLAYFIRST_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAYFIRST_USAGE'), + description: i18next.t('commands:CONFIG_PLAYFIRST_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAYFIRST_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -27,7 +29,7 @@ export class PlayFirstCommand extends BaseCommand { options: [ { name: 'playfirst', - description: i18next.t('commands:CONFIG_PLAYFIRST_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAYFIRST_OPTION_DESCRIPTION', { lng }), type: 3, required: true } @@ -37,22 +39,31 @@ export class PlayFirstCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { // Get search query - const str = context.isMessage() + const rawStr = context.isMessage() ? context.args.join(' ') : context.getStringOption('playfirst'); + const str = cleanSearchQuery(rawStr || ''); + if (!str) { await context.replyEphemeralError(bot, context.t('commands:MESSAGE_PLAY_ARGS_ERROR')); return; } + const isM3uUrl = /\.m3u8?(\?.*)?$/i.test(str); + const existingPlaylist = bot.playlistManager?.getPlaylist(context.guild!.id, str); + if (isM3uUrl || existingPlaylist?.isM3u) { + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL')); + return; + } + // Search for tracks let res; try { res = await client.lavashark.search(str); } catch (error) { console.error(error); - bot.logger.error( bot.shardId, `Search Error: ${error}`); + bot.logger.error(bot.shardId, `Search Error: ${error}`); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_SEARCH', { reason: error instanceof Error ? error.message : String(error) })); @@ -61,7 +72,7 @@ export class PlayFirstCommand extends BaseCommand { // Handle search results if (res.loadType === LoadType.ERROR) { - bot.logger.error( bot.shardId, `Search Error: ${JSON.stringify(res)}`); + bot.logger.error(bot.shardId, `Search Error: ${JSON.stringify(res)}`); await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_SEARCH', { reason: (res as any).data?.message })); @@ -109,20 +120,28 @@ export class PlayFirstCommand extends BaseCommand { } /** - * Create and initialize player + * Create and initialize player (or reuse existing player) * @private */ async #createPlayer(bot: Bot, client: Client, context: CommandContext): Promise { + const guildId = String(context.guild?.id); const voiceChannelId = context.isMessage() ? String(context.getMessage().member?.voice.channelId) : String(context.getInteraction().guild!.members.cache.get(context.user.id)?.voice.channelId); - const player = client.lavashark.createPlayer({ - guildId: String(context.guild?.id), - voiceChannelId: voiceChannelId, - textChannelId: context.channel!.id, - selfDeaf: true - }); + let player = client.lavashark.getPlayer(guildId); + + if (!player) { + player = client.lavashark.createPlayer({ + guildId: guildId, + voiceChannelId: voiceChannelId, + textChannelId: context.channel!.id, + selfDeaf: true + }); + } else { + player.voiceChannelId = voiceChannelId; + player.textChannelId = context.channel!.id; + } if (!player.setting) { player.setting = { @@ -132,16 +151,18 @@ export class PlayFirstCommand extends BaseCommand { }; } - const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - - try { - await player.connect(); - player.metadata = metadata; - } catch (error) { - bot.logger.error( bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - return null; + player.metadata = metadata; + + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + } catch (error) { + bot.logger.error(bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return null; + } } try { @@ -237,6 +258,10 @@ export class PlayFirstCommand extends BaseCommand { const requester = context.isMessage() ? context.getMessage().author : context.getInteraction().user; const curVolume = player.setting.volume ?? bot.guildVolumeManager?.get(player.guildId) ?? bot.config.bot.volume.default; + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + if (res.loadType === LoadType.PLAYLIST) { const tracks = tracksToAdd !== undefined ? res.tracks.slice(0, tracksToAdd) : res.tracks; @@ -245,6 +270,15 @@ export class PlayFirstCommand extends BaseCommand { } if (!player.playing) { + player.addTracks(tracks, requester as any); + player.filters.setVolume(curVolume); + await player.play() + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + } else { const firstTrack = tracks[0]; const restTracks = tracks.slice(1); @@ -259,21 +293,35 @@ export class PlayFirstCommand extends BaseCommand { await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); return player.destroy(); }); - } else { - player.queue.tracks.unshift(...tracks); - if (player.current && client.dashboard) { - await client.dashboard.update(player, player.current); - } } } else { const track = res.tracks[0]; - await player.prioritizePlay(track, requester as any) - .catch(async (error) => { - bot.logger.error(bot.shardId, 'Error playing track: ' + error); - await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); - return player.destroy(); - }); + (track as any).requester = requester; + + if (!player.playing) { + player.addTracks(track, requester as any); + player.filters.setVolume(curVolume); + await player.play() + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + } else { + player.filters.setVolume(curVolume); + await player.prioritizePlay(track, requester as any) + .catch(async (error) => { + bot.logger.error(bot.shardId, 'Error playing track: ' + error); + await context.replyError(bot, context.t('commands:ERROR_PLAY_MUSIC', { reason: JSON.stringify(error) })); + return player.destroy(); + }); + } + } + + if (player.current && client.dashboard) { + await client.dashboard.update(player, player.current); } } } + diff --git a/src/commands/PlayLastCommand.ts b/src/commands/PlayLastCommand.ts index de218bd3..813d9970 100644 --- a/src/commands/PlayLastCommand.ts +++ b/src/commands/PlayLastCommand.ts @@ -15,12 +15,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * PlayLast command - Replay the current or last played song */ export class PlayLastCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'playlast', aliases: [], - description: i18next.t('commands:CONFIG_PLAYLAST_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_PLAYLAST_USAGE'), + description: i18next.t('commands:CONFIG_PLAYLAST_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_PLAYLAST_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: false, showHelp: true, diff --git a/src/commands/PlaylistCommand.ts b/src/commands/PlaylistCommand.ts index 1d161616..a58fc94f 100644 --- a/src/commands/PlaylistCommand.ts +++ b/src/commands/PlaylistCommand.ts @@ -41,24 +41,29 @@ export class PlaylistCommand extends BaseCommand { /** * Build playlist command metadata and slash command options */ - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { aliases: ['pl'], category: CommandCategory.MUSIC, - description: i18next.t('commands:CONFIG_PLAYLIST_DESCRIPTION'), + description: i18next.t('commands:CONFIG_PLAYLIST_DESCRIPTION', { lng }), name: 'playlist', options: [ this.createNamedSubcommand( 'save', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE', + true, + lng ), this.createNamedSubcommand( 'play', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY', + true, + lng ), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST', + { lng } ), name: 'list', type: ApplicationCommandOptionType.Subcommand, @@ -66,21 +71,27 @@ export class PlaylistCommand extends BaseCommand { this.createNamedSubcommand( 'info', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO', + true, + lng ), this.createNamedSubcommand( 'delete', 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE', + true, + lng ), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT', + { lng } ), name: 'import', options: [ - this.createNameOption(), + this.createNameOption(true, lng), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_URL', + { lng } ), name: 'url', required: true, @@ -92,13 +103,15 @@ export class PlaylistCommand extends BaseCommand { { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK', + { lng } ), name: 'remove-track', options: [ - this.createNameOption(), + this.createNameOption(true, lng), { description: i18next.t( 'commands:CONFIG_PLAYLIST_OPTION_INDEX', + { lng } ), name: 'index', required: true, @@ -110,7 +123,7 @@ export class PlaylistCommand extends BaseCommand { ], sendTyping: true, showHelp: true, - usage: i18next.t('commands:CONFIG_PLAYLIST_USAGE'), + usage: i18next.t('commands:CONFIG_PLAYLIST_USAGE', { lng }), voiceChannel: false, }; } @@ -123,28 +136,31 @@ export class PlaylistCommand extends BaseCommand { client: Client, command: CommandContext, ): Promise { - // Ensure playlist storage is available before dispatching - const playlistManager = bot.playlistManager; - if (!playlistManager) { + if (bot.config.playlist?.enabled === false) { await command.replyEphemeralError( bot, - command.t('commands:ERROR_PLAYLIST_NOT_INITIALIZED'), + command.t('commands:ERROR_PLAYLIST_DISABLED'), ); return; } - // Normalize the subcommand name from either command source - const name = command.isMessage() - ? command.args[0]?.toLowerCase() - : command.getInteraction().options.getSubcommand(false); - if (!name) { + // Ensure playlist storage is available before dispatching + const playlistManager = bot.playlistManager; + if (!playlistManager) { await command.replyEphemeralError( bot, - command.t('commands:ERROR_PLAYLIST_SUBCOMMAND_INVALID'), + command.t('commands:ERROR_PLAYLIST_NOT_INITIALIZED'), ); return; } + // Normalize the subcommand name from either command source (default to 'list') + const name = ( + command.isMessage() + ? command.args[0]?.toLowerCase() + : command.getInteraction().options.getSubcommand(false) + ) || 'list'; + // Find the matching OOP subcommand implementation const subcommand = this.#subcommands.find( (candidate) => candidate.name === name, @@ -166,11 +182,13 @@ export class PlaylistCommand extends BaseCommand { private createNamedSubcommand( name: PlaylistSubcommandName, descriptionKey: string, + nameRequired: boolean = true, + lng?: string ): ApplicationCommandSubCommandData { return { - description: i18next.t(descriptionKey), + description: i18next.t(descriptionKey, { lng }), name, - options: [this.createNameOption()], + options: [this.createNameOption(nameRequired, lng)], type: ApplicationCommandOptionType.Subcommand, }; } @@ -178,11 +196,11 @@ export class PlaylistCommand extends BaseCommand { /** * Create the shared playlist name option */ - private createNameOption(): ApplicationCommandStringOptionData { + private createNameOption(required: boolean = true, lng?: string): ApplicationCommandStringOptionData { return { - description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME'), + description: i18next.t('commands:CONFIG_PLAYLIST_OPTION_NAME', { lng }), name: 'name', - required: true, + required, type: ApplicationCommandOptionType.String, }; } diff --git a/src/commands/QueueCommand.ts b/src/commands/QueueCommand.ts index a199e1ac..704ba6f8 100644 --- a/src/commands/QueueCommand.ts +++ b/src/commands/QueueCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class QueueCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'queue', aliases: ['q', 'list'], - description: i18next.t('commands:CONFIG_QUEUE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_QUEUE_USAGE'), + description: i18next.t('commands:CONFIG_QUEUE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_QUEUE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/RadioCommand.ts b/src/commands/RadioCommand.ts index be045102..a0a61864 100644 --- a/src/commands/RadioCommand.ts +++ b/src/commands/RadioCommand.ts @@ -1,21 +1,23 @@ import i18next from 'i18next'; import { ApplicationCommandOptionType } from 'discord.js'; +import { ConnectionState, RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum } from '../@types/index.js'; import { DJManager } from '../lib/DjManager.js'; +import { decodeTrackWithRetry } from '../utils/functions/lavasharkRequest.js'; import type { Client, GuildMember } from 'discord.js'; import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class RadioCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'radio', - aliases: ['record'], - description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_RADIO_USAGE'), + aliases: [], + description: i18next.t('commands:CONFIG_RADIO_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_RADIO_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,13 +25,13 @@ export class RadioCommand extends BaseCommand { options: [ { name: 'playlist', - description: i18next.t('commands:CONFIG_RADIO_OPTION_PLAYLIST'), + description: i18next.t('commands:CONFIG_RADIO_OPTION_PLAYLIST', { lng }), type: ApplicationCommandOptionType.String, required: true }, { name: 'channel', - description: i18next.t('commands:CONFIG_RADIO_OPTION_CHANNEL'), + description: i18next.t('commands:CONFIG_RADIO_OPTION_CHANNEL', { lng }), type: ApplicationCommandOptionType.String, required: true } @@ -122,13 +124,18 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - try { - await player.connect(); + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + player.metadata = metadata; + } catch (error) { + bot.logger.error(bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return; + } + } else { player.metadata = metadata; - } catch (error) { - bot.logger.error(bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - return; } try { @@ -154,21 +161,33 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; await context.replySuccess(bot, context.t('commands:MESSAGE_RADIO_SEARCHING', { playlist: playlistName, title: targetTrack.title })); try { - let searchResult = null; - if (targetTrack.url) { + let radioTrack = null; + if (targetTrack.encoded) { + try { + radioTrack = await decodeTrackWithRetry(client.lavashark, targetTrack.encoded); + } catch (_) {} + } + + if (!radioTrack && targetTrack.url) { try { - searchResult = await client.lavashark.search(targetTrack.url); + const result = await client.lavashark.search(targetTrack.url); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + radioTrack = result.tracks[0]; + } } catch (_) {} } - if (!searchResult || !searchResult.tracks || searchResult.tracks.length === 0) { + if (!radioTrack && targetTrack.title) { try { - searchResult = await client.lavashark.search(`ytsearch:${targetTrack.title}`); + const result = await client.lavashark.search(`ytsearch:${targetTrack.title}`); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + radioTrack = result.tracks[0]; + } } catch (_) {} } - if (searchResult && searchResult.tracks && searchResult.tracks.length > 0) { - const track = searchResult.tracks[0]; + if (radioTrack) { + const track = radioTrack; (track as any).requester = requester; const isAlreadyPlaying = player.playing; @@ -179,6 +198,9 @@ const DEFAULT_MAX_SAMPLES_COUNT = 10; (track as any).isRadio = true; if (isAlreadyPlaying) { + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } await player.skip(); } else { player.filters.setVolume(curVolume); diff --git a/src/commands/RemoveCommand.ts b/src/commands/RemoveCommand.ts index a5bd541c..669dca2b 100644 --- a/src/commands/RemoveCommand.ts +++ b/src/commands/RemoveCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class RemoveCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'remove', aliases: ['rm'], - description: i18next.t('commands:CONFIG_REMOVE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_REMOVE_USAGE'), + description: i18next.t('commands:CONFIG_REMOVE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_REMOVE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -23,13 +23,13 @@ export class RemoveCommand extends BaseCommand { options: [ { name: 'index', - description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION', { lng }), type: 10, required: false }, { name: 'index2', - description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION_2'), + description: i18next.t('commands:CONFIG_REMOVE_OPTION_DESCRIPTION_2', { lng }), type: 10, required: false } @@ -209,6 +209,8 @@ export class RemoveCommand extends BaseCommand { }); collector.on('collect', async (query: Message) => { + collector.stop(); + const index = parseInt(query.content); if (!index || index <= 0 || index > tracks.length) { @@ -224,7 +226,6 @@ export class RemoveCommand extends BaseCommand { allowedMentions: { repliedUser: false } }); } - collector.stop(); return; } @@ -243,8 +244,6 @@ export class RemoveCommand extends BaseCommand { msg.delete().catch(() => bot.logger.discord( bot.shardId, 'Failed to delete message.') ); - - collector.stop(); }); collector.on('end', async (collected: ReadonlyCollection>, reason: string) => { diff --git a/src/commands/ResumeCommand.ts b/src/commands/ResumeCommand.ts index 8ead4338..1d412913 100644 --- a/src/commands/ResumeCommand.ts +++ b/src/commands/ResumeCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ResumeCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'resume', aliases: [], - description: i18next.t('commands:CONFIG_RESUME_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_RESUME_USAGE'), + description: i18next.t('commands:CONFIG_RESUME_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_RESUME_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index f6921a79..cc33009e 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -6,11 +6,13 @@ import { StringSelectMenuInteraction, } from 'discord.js'; import i18next from 'i18next'; +import { ConnectionState } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory, DJModeEnum, LoadType, SelectButtonId } from '../@types/index.js'; import { embeds } from '../embeds/index.js'; import { isUserInBlacklist } from '../utils/functions/isUserInBlacklist.js'; +import { cleanSearchQuery } from '../utils/functions/cleanSearchQuery.js'; import { DJManager } from '../lib/DjManager.js'; import { QueueLimitManager } from '../lib/QueueLimitManager.js'; @@ -21,12 +23,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class SearchCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'search', aliases: ['find'], - description: i18next.t('commands:CONFIG_SEARCH_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SEARCH_USAGE'), + description: i18next.t('commands:CONFIG_SEARCH_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SEARCH_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -34,7 +36,7 @@ export class SearchCommand extends BaseCommand { options: [ { name: 'search', - description: i18next.t('commands:CONFIG_SEARCH_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_SEARCH_OPTION_DESCRIPTION', { lng }), type: 3, required: true } @@ -44,10 +46,12 @@ export class SearchCommand extends BaseCommand { protected async run(bot: Bot, client: Client, context: CommandContext): Promise { // Get search query - const str = context.isMessage() + const rawStr = context.isMessage() ? context.args.join(' ') : context.getStringOption('search'); + const str = cleanSearchQuery(rawStr || ''); + if (!str) { await context.replyEphemeralError(bot, context.t('commands:MESSAGE_PLAY_ARGS_ERROR')); return; @@ -119,20 +123,28 @@ export class SearchCommand extends BaseCommand { } /** - * Create and initialize player + * Create and initialize player (or reuse existing player) * @private */ async #createPlayer(bot: Bot, client: Client, context: CommandContext): Promise { + const guildId = String(context.guild?.id); const voiceChannelId = context.isMessage() ? String(context.getMessage().member?.voice.channelId) : String(context.getInteraction().guild!.members.cache.get(context.user.id)?.voice.channelId); - const player = client.lavashark.createPlayer({ - guildId: String(context.guild?.id), - voiceChannelId: voiceChannelId, - textChannelId: context.channel!.id, - selfDeaf: true - }); + let player = client.lavashark.getPlayer(guildId); + + if (!player) { + player = client.lavashark.createPlayer({ + guildId: guildId, + voiceChannelId: voiceChannelId, + textChannelId: context.channel!.id, + selfDeaf: true + }); + } else { + player.voiceChannelId = voiceChannelId; + player.textChannelId = context.channel!.id; + } if (!player.setting) { player.setting = { @@ -143,14 +155,17 @@ export class SearchCommand extends BaseCommand { } const metadata = context.isMessage() ? context.getMessage() : context.getInteraction(); - - try { - await player.connect(); - player.metadata = metadata; - } catch (error) { - bot.logger.error( bot.shardId, 'Error joining channel: ' + error); - await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); - return null; + player.metadata = metadata; + + if (player.state !== ConnectionState.CONNECTED) { + try { + await player.connect(); + } catch (error) { + bot.logger.error( bot.shardId, 'Error joining channel: ' + error); + await context.replyEphemeralError(bot, context.t('commands:ERROR_PLAY_JOIN_CHANNEL')); + await player.destroy(); + return null; + } } try { @@ -297,7 +312,7 @@ export class SearchCommand extends BaseCommand { }); collector.on('collect', async (i: StringSelectMenuInteraction) => { - if (i.customId != SelectButtonId.Music) return; + if (i.customId !== SelectButtonId.Music) return; // Check queue limits before adding selected track const checkResult = QueueLimitManager.canAddSongs(bot, player, userId, guildMember, 1); @@ -317,7 +332,7 @@ export class SearchCommand extends BaseCommand { const requester = context.isMessage() ? context.getMessage().author : context.getInteraction().user; const curVolume = player.setting.volume ?? bot.guildVolumeManager?.get(player.guildId) ?? bot.config.bot.volume.default; - player.addTracks(res.tracks.find((x: any) => x.uri == i.values[0])!, requester as any); + player.addTracks(res.tracks.find((x: any) => x.uri === i.values[0])!, requester as any); if (!player.playing) { player.filters.setVolume(curVolume); @@ -344,7 +359,7 @@ export class SearchCommand extends BaseCommand { }); collector.on('end', async (collected: Collection, reason: string) => { - if (reason == 'time' && collected.size == 0) { + if (reason === 'time' && collected.size === 0) { if (!player.playing) { player.destroy(); } @@ -357,3 +372,4 @@ export class SearchCommand extends BaseCommand { }); } } + diff --git a/src/commands/SeekCommand.ts b/src/commands/SeekCommand.ts index 9fa19ed7..e215e0ba 100644 --- a/src/commands/SeekCommand.ts +++ b/src/commands/SeekCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class SeekCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'seek', aliases: [], - description: i18next.t('commands:CONFIG_SEEK_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SEEK_USAGE'), + description: i18next.t('commands:CONFIG_SEEK_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SEEK_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -24,7 +24,7 @@ export class SeekCommand extends BaseCommand { options: [ { name: 'seek', - description: i18next.t('commands:CONFIG_SEEK_OPTION_DESCRIPTION'), + description: i18next.t('commands:CONFIG_SEEK_OPTION_DESCRIPTION', { lng }), type: 3, required: true } diff --git a/src/commands/ServerCommand.ts b/src/commands/ServerCommand.ts index d9fb0926..05349102 100644 --- a/src/commands/ServerCommand.ts +++ b/src/commands/ServerCommand.ts @@ -11,12 +11,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ServerCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'server', aliases: [], - description: i18next.t('commands:CONFIG_SERVER_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SERVER_USAGE'), + description: i18next.t('commands:CONFIG_SERVER_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SERVER_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, diff --git a/src/commands/ShuffleCommand.ts b/src/commands/ShuffleCommand.ts index f05c17e0..ae85a949 100644 --- a/src/commands/ShuffleCommand.ts +++ b/src/commands/ShuffleCommand.ts @@ -9,12 +9,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class ShuffleCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'shuffle', aliases: ['random'], - description: i18next.t('commands:CONFIG_SHUFFLE_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SHUFFLE_USAGE'), + description: i18next.t('commands:CONFIG_SHUFFLE_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SHUFFLE_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/SkipCommand.ts b/src/commands/SkipCommand.ts index e0b2350e..ddead2bc 100644 --- a/src/commands/SkipCommand.ts +++ b/src/commands/SkipCommand.ts @@ -1,4 +1,5 @@ import i18next from 'i18next'; +import { RepeatMode } from 'lavashark'; import { BaseCommand } from './base/BaseCommand.js'; import { CommandCategory } from '../@types/index.js'; @@ -13,12 +14,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; * Skip command - Skips current track */ export class SkipCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'skip', aliases: ['s'], - description: i18next.t('commands:CONFIG_SKIP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_SKIP_USAGE'), + description: i18next.t('commands:CONFIG_SKIP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_SKIP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -61,6 +62,11 @@ export class SkipCommand extends BaseCommand { } const hasMoreTracks = player.queue.tracks.length > 0 || player.repeatMode !== 0; + + if (player.repeatMode === RepeatMode.TRACK) { + player.setRepeatMode(RepeatMode.OFF); + } + const success = await player.skip(); if (context.isMessage()) { diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index fb6c03ce..ffc223a7 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -13,12 +13,12 @@ import type { Bot, CommandMetadata, SystemStatus } from '../@types/index.js'; export class StatusCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'status', aliases: ['info'], - description: i18next.t('commands:CONFIG_STATUS_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_STATUS_USAGE'), + description: i18next.t('commands:CONFIG_STATUS_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_STATUS_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -28,7 +28,7 @@ export class StatusCommand extends BaseCommand { } protected async run(bot: Bot, client: Client, context: CommandContext): Promise { - const botPing = `${Date.now() - context.createdTimestamp}ms`; + const botPing = `${Math.abs(Date.now() - context.createdTimestamp)}ms`; const sysload = await sysusage.cpu(); const pingList = await client.lavashark.nodesPing(); diff --git a/src/commands/StopCommand.ts b/src/commands/StopCommand.ts index c7dbded1..aef16cf9 100644 --- a/src/commands/StopCommand.ts +++ b/src/commands/StopCommand.ts @@ -10,12 +10,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class StopCommand extends BaseCommand { - public getMetadata(_bot: Bot): CommandMetadata { + public getMetadata(_bot: Bot, lng?: string): CommandMetadata { return { name: 'stop', aliases: [], - description: i18next.t('commands:CONFIG_STOP_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_STOP_USAGE'), + description: i18next.t('commands:CONFIG_STOP_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_STOP_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, diff --git a/src/commands/VolumeCommand.ts b/src/commands/VolumeCommand.ts index 39b607c7..f7ec42a0 100644 --- a/src/commands/VolumeCommand.ts +++ b/src/commands/VolumeCommand.ts @@ -19,12 +19,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js'; export class VolumeCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { return { name: 'volume', aliases: ['v'], - description: i18next.t('commands:CONFIG_VOLUME_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_VOLUME_USAGE'), + description: i18next.t('commands:CONFIG_VOLUME_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_VOLUME_USAGE', { lng }), category: CommandCategory.MUSIC, voiceChannel: true, showHelp: true, @@ -32,7 +32,7 @@ export class VolumeCommand extends BaseCommand { options: [ { name: 'volume', - description: bot.i18n.t('commands:CONFIG_VOLUME_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max }), + description: bot.i18n.t('commands:CONFIG_VOLUME_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max, lng }), type: 4, required: false, min_value: 1, diff --git a/src/commands/VolumeDefaultCommand.ts b/src/commands/VolumeDefaultCommand.ts index bddf7ab8..9beb8fce 100644 --- a/src/commands/VolumeDefaultCommand.ts +++ b/src/commands/VolumeDefaultCommand.ts @@ -8,12 +8,12 @@ import type { CommandContext } from './base/CommandContext.js'; import type { Bot, CommandMetadata } from '../@types/index.js'; export class VolumeDefaultCommand extends BaseCommand { - public getMetadata(bot: Bot): CommandMetadata { + public getMetadata(bot: Bot, lng?: string): CommandMetadata { return { name: 'volume-default', aliases: ['vdef', 'voldef'], - description: i18next.t('commands:CONFIG_VOLUME_DEFAULT_DESCRIPTION'), - usage: i18next.t('commands:CONFIG_VOLUME_DEFAULT_USAGE'), + description: i18next.t('commands:CONFIG_VOLUME_DEFAULT_DESCRIPTION', { lng }), + usage: i18next.t('commands:CONFIG_VOLUME_DEFAULT_USAGE', { lng }), category: CommandCategory.UTILITY, voiceChannel: false, showHelp: true, @@ -21,7 +21,7 @@ export class VolumeDefaultCommand extends BaseCommand { options: [ { name: 'volume', - description: bot.i18n.t('commands:CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max }), + description: bot.i18n.t('commands:CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION', { maxVolume: bot.config.bot.volume.max, lng }), type: 4, required: false, min_value: 1, diff --git a/src/commands/base/BaseCommand.ts b/src/commands/base/BaseCommand.ts index 67eab573..a08b671b 100644 --- a/src/commands/base/BaseCommand.ts +++ b/src/commands/base/BaseCommand.ts @@ -12,7 +12,7 @@ export abstract class BaseCommand { /** * Get command metadata */ - public abstract getMetadata(bot: Bot): CommandMetadata; + public abstract getMetadata(bot: Bot, lng?: string): CommandMetadata; /** * Execute command logic (implemented by subclasses) diff --git a/src/commands/base/CommandRegistry.ts b/src/commands/base/CommandRegistry.ts index 3a74d7c9..f43f4be0 100644 --- a/src/commands/base/CommandRegistry.ts +++ b/src/commands/base/CommandRegistry.ts @@ -77,9 +77,9 @@ export class CommandRegistry { /** * Get all commands that should be shown in help */ - public getHelpCommands(bot: Bot): BaseCommand[] { + public getHelpCommands(bot: Bot, lng?: string): BaseCommand[] { return this.getAll().filter(cmd => - cmd.getMetadata(bot).showHelp + cmd.getMetadata(bot, lng).showHelp ); } } diff --git a/src/commands/playlist/ImportPlaylistSubcommand.ts b/src/commands/playlist/ImportPlaylistSubcommand.ts index 73962555..92747443 100644 --- a/src/commands/playlist/ImportPlaylistSubcommand.ts +++ b/src/commands/playlist/ImportPlaylistSubcommand.ts @@ -1,4 +1,5 @@ import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; +import { DJManager } from '../../lib/DjManager.js'; import { PLAYLIST_TRACK_LIMIT } from '../../lib/PlaylistManager.js'; import { DEFAULT_REMOTE_TEXT_MAX_BYTES, @@ -23,6 +24,18 @@ export class ImportPlaylistSubcommand extends BasePlaylistSubcommand { * Validate import arguments and confirm replacement when required */ public async execute(context: PlaylistSubcommandContext): Promise { + const userId = context.command.user.id; + const member = context.command.member; + const player = context.client.lavashark.getPlayer(context.command.guild!.id); + + if (!DJManager.isDJ(context.bot, userId, member, player ?? undefined)) { + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY'), + ); + return; + } + const args = this.getImportArguments(context.command); if (!args) { await context.command.replyEphemeralError( diff --git a/src/commands/playlist/PlayPlaylistSubcommand.ts b/src/commands/playlist/PlayPlaylistSubcommand.ts index 754e02be..475ebbaa 100644 --- a/src/commands/playlist/PlayPlaylistSubcommand.ts +++ b/src/commands/playlist/PlayPlaylistSubcommand.ts @@ -1,11 +1,18 @@ -import { DJModeEnum } from '../../@types/index.js'; +import { DJModeEnum, LoadType } from '../../@types/index.js'; import { embeds } from '../../embeds/index.js'; import { DJManager } from '../../lib/DjManager.js'; import { QueueLimitManager } from '../../lib/QueueLimitManager.js'; import { isUserInBlacklist } from '../../utils/functions/isUserInBlacklist.js'; +import { + decodeTracksWithRetry, + decodeTrackWithRetry, + searchWithRetry, + sleep, +} from '../../utils/functions/lavasharkRequest.js'; import { BasePlaylistSubcommand } from './BasePlaylistSubcommand.js'; import type { GuildMember, VoiceBasedChannel } from 'discord.js'; +import { RepeatMode } from 'lavashark'; import type { Player } from 'lavashark'; import type { Playlist, PlaylistTrack } from '../../lib/PlaylistManager.js'; import type { @@ -52,6 +59,14 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { return; } + if (playlist.isM3u) { + await context.command.replyEphemeralError( + context.bot, + context.command.t('commands:ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL'), + ); + return; + } + await this.play(context, playlist); } @@ -69,6 +84,13 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const player = await this.initializePlayer(context, voiceChannel); if (!player) return; + // A radio session is replaced by the playlist: drop its queued tracks + // so the playlist content plays next instead of the radio station + const isRadioPlaying = Boolean(player.current && (player.current as any).isRadio); + if (isRadioPlaying) { + player.queue.tracks = []; + } + const tracks = playlist.tracks ?? []; // Reuse one progress message for the final load result const progressMessage = await context.command.reply({ @@ -82,7 +104,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { const result = await this.loadTracks(context, player, playlist, member); if (result.added > 0) { - await this.startPlayback(context, player); + await this.startPlayback(context, player, isRadioPlaying); if (context.bot.config.queuePersistence.enabled && context.client.queuePersistence) { await context.client.queuePersistence.saveQueue(player); @@ -185,7 +207,9 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { ? command.getMessage() : command.getInteraction(); try { - await player.connect(); + if (!player.voiceChannelId || player.voiceChannelId !== voiceChannel.id) { + await player.connect(); + } player.metadata = metadata; } catch (error) { bot.logger.error( @@ -196,6 +220,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { bot, command.t('commands:ERROR_PLAY_JOIN_CHANNEL'), ); + await player.destroy(); return null; } @@ -221,24 +246,41 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { } /** - * Resolve a stored track using encoded data, URL, then title search + * Resolve a stored track using encoded data, URL, then title search. + * YouTube mix/playlist URLs (`watch?v=X&list=...`) resolve as a + * PLAYLIST result, so the whole playlist is expanded into the queue. */ private async findTrack( context: PlaylistSubcommandContext, playlistTrack: PlaylistTrack, - ) { + ): Promise { + if (playlistTrack.encoded) { + const track = await decodeTrackWithRetry( + context.client.lavashark, + playlistTrack.encoded, + ); + if (track) return [track]; + } + const queries = [ - playlistTrack.encoded, playlistTrack.url, `ytsearch:${playlistTrack.title}`, ].filter((query): query is string => Boolean(query)); for (const query of new Set(queries)) { - try { - const result = await context.client.lavashark.search(query); - if (result.tracks.length > 0) return result.tracks[0]; - } catch { - // Try the next fallback query. + const result = await searchWithRetry(context.client.lavashark, query); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + if (result.loadType === LoadType.PLAYLIST) { + // Mix/playlist expansion keeps the real track names + return result.tracks; + } + const track = result.tracks[0]; + // Keep the name exactly as it was saved, even if the fallback + // search resolved to a different video + if (playlistTrack.title) track.title = playlistTrack.title; + if (playlistTrack.author) track.author = playlistTrack.author; + if (playlistTrack.url) track.uri = playlistTrack.url; + return [track]; } } @@ -246,7 +288,11 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { } /** - * Resolve and enqueue tracks while respecting queue limits + * Resolve and enqueue tracks while respecting queue limits. + * Tracks carrying a saved encoded string are bulk-decoded in a single + * request (`/decodetracks`) — Lavalink nodes rate-limit REST calls + * (HTTP 429), and bursting per-track requests makes tracks get skipped. + * Entries without an encoded string fall back to paced per-track lookup. */ private async loadTracks( context: PlaylistSubcommandContext, @@ -256,39 +302,69 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { ): Promise { let added = 0; let skipped = 0; - - for (const playlistTrack of playlist.tracks ?? []) { - // Check both per-user and global limits before each search - const limit = QueueLimitManager.canAddSongs( - context.bot, - player, - context.command.user.id, - member, - 1, + const tracks = playlist.tracks ?? []; + const TRACK_REQUEST_DELAY_MS = 400; + + const canAdd = (): boolean => QueueLimitManager.canAddSongs( + context.bot, + player, + context.command.user.id, + member, + 1, + ).canAdd; + + const entries = tracks.map((track, index) => ({ track, index })); + const resolvedByIndex: Map = new Map(); + + // Bulk-decode entries that carry a saved encoded track string + const encodedEntries = entries.filter(({ track }) => + track.encoded && track.encoded.trim() !== '', + ); + if (encodedEntries.length > 0) { + const decoded = await decodeTracksWithRetry( + context.client.lavashark, + encodedEntries.map(({ track }) => track.encoded!.trim()), ); - if (!limit.canAdd) { - skipped++; + if (decoded && decoded.length === encodedEntries.length) { + encodedEntries.forEach(({ track, index }, i) => { + if (decoded[i] && canAdd()) { + // Keep the names exactly as they were saved + if (track.title) decoded[i].title = track.title; + if (track.author) decoded[i].author = track.author; + if (track.url) decoded[i].uri = track.url; + resolvedByIndex.set(index, [decoded[i]]); + added++; + } + }); + } + } + + // Per-track fallback for entries not covered by the bulk decode + for (const { track, index } of entries) { + if (resolvedByIndex.has(index)) { continue; } - try { - const track = await this.findTrack(context, playlistTrack); - if (!track) { - skipped++; - continue; - } + if (!canAdd()) { + skipped++; + await sleep(TRACK_REQUEST_DELAY_MS); + continue; + } - // LavaShark resolves discord.js through a separate type entry point. - const requester = context.command.user as unknown as PlayerRequester; - player.addTracks(track, requester); - added++; - } catch (error) { - context.bot.logger.error( - context.bot.shardId, - `[PlaylistCommand] Error loading track ${playlistTrack.title}: ${error}`, - ); + const result = await this.findTrack(context, track); + if (result && result.length > 0) { + resolvedByIndex.set(index, result); + added += result.length; + } else { skipped++; } + await sleep(TRACK_REQUEST_DELAY_MS); + } + + const resolvedTracks = entries.flatMap(({ index }) => resolvedByIndex.get(index) ?? []); + if (resolvedTracks.length > 0) { + const requester = context.command.user as unknown as PlayerRequester; + player.addTracks(resolvedTracks, requester); } return { added, skipped }; @@ -300,6 +376,7 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { private async startPlayback( context: PlaylistSubcommandContext, player: Player, + stopRadio = false, ): Promise { if (!player.playing) { const volume = player.setting.volume ?? @@ -307,18 +384,42 @@ export class PlayPlaylistSubcommand extends BasePlaylistSubcommand { context.bot.config.bot.volume.default; player.filters.setVolume(volume); - try { - await player.play(); - } catch (error) { - context.bot.logger.error( - context.bot.shardId, - `[PlaylistCommand] Error playing track: ${error}`, - ); - await player.destroy(); + // Retry playback a few times so a transient rate limit (HTTP 429) + // does not destroy the player right after loading a playlist + const PLAY_RETRY_ATTEMPTS = 3; + const PLAY_RETRY_DELAY_MS = 2000; + for (let attempt = 0; attempt < PLAY_RETRY_ATTEMPTS; attempt++) { + try { + await player.play(); + return; + } catch (error) { + if (attempt < PLAY_RETRY_ATTEMPTS - 1) { + context.bot.logger.log( + context.bot.shardId, + `[PlaylistCommand] Error playing track (attempt ${attempt + 1}/${PLAY_RETRY_ATTEMPTS}): ${error}`, + ); + await sleep(PLAY_RETRY_DELAY_MS); + continue; + } + context.bot.logger.error( + context.bot.shardId, + `[PlaylistCommand] Error playing track: ${error}`, + ); + await player.destroy(); + } } return; } + // Stop the active radio and let the playlist take over: any repeat + // would otherwise keep the radio track in the queue or replay it + if (stopRadio) { + if (player.repeatMode !== RepeatMode.OFF) { + player.setRepeatMode(RepeatMode.OFF); + } + await player.skip(); + } + if (player.current) { await context.client.dashboard.update(player, player.current); } diff --git a/src/commands/playlist/ToggleM3uSubcommand.ts b/src/commands/playlist/ToggleM3uSubcommand.ts new file mode 100644 index 00000000..9cec1e73 --- /dev/null +++ b/src/commands/playlist/ToggleM3uSubcommand.ts @@ -0,0 +1,2 @@ +// Deprecated subcommand, replaced by Admin/DJ permission check on import and radio-only station selection. +export {}; diff --git a/src/events/discord/InteractionCreateEvent.ts b/src/events/discord/InteractionCreateEvent.ts index cfbb6f18..318cbd8b 100644 --- a/src/events/discord/InteractionCreateEvent.ts +++ b/src/events/discord/InteractionCreateEvent.ts @@ -13,6 +13,7 @@ import { ShuffleButtonHandler } from '../../lib/handlers/ShuffleButtonHandler.js import { MusicSaveButtonHandler } from '../../lib/handlers/MusicSaveButtonHandler.js'; import { QueueButtonHandler } from '../../lib/handlers/QueueButtonHandler.js'; import { LanguageSelectHandler } from '../../lib/handlers/LanguageSelectHandler.js'; +import { NodeSelectHandler } from '../../lib/handlers/NodeSelectHandler.js'; import { DashboardButtonId, QueueButtonId, MusicButtonId } from '../../@types/index.js'; import type { Client } from 'discord.js'; @@ -40,6 +41,7 @@ export class InteractionCreateEvent extends BaseDiscordEvent {}); + } + } } } diff --git a/src/events/discord/VoiceStateUpdateEvent.ts b/src/events/discord/VoiceStateUpdateEvent.ts index 672674ff..d94676e2 100644 --- a/src/events/discord/VoiceStateUpdateEvent.ts +++ b/src/events/discord/VoiceStateUpdateEvent.ts @@ -211,7 +211,10 @@ export class VoiceStateUpdateEvent extends BaseDiscordEvent { } public execute(bot: Bot, _client: Client, node: Node, error: any): void { - bot.logger.error( bot.shardId, `[LavaShark] ${node.identifier} error: ${error.message}`); + // `node` can be null at runtime: lavashark may emit 'error' after the + // node was detached from a destroyed player + bot.logger.error( + bot.shardId, + `[LavaShark] ${node?.identifier ?? 'unknown'} error: ${error?.message ?? error}`, + ); } } diff --git a/src/events/lavashark/PlayerDestroyEvent.ts b/src/events/lavashark/PlayerDestroyEvent.ts index 9c2e4186..de54e8f5 100644 --- a/src/events/lavashark/PlayerDestroyEvent.ts +++ b/src/events/lavashark/PlayerDestroyEvent.ts @@ -38,9 +38,11 @@ export class PlayerDestroyEvent extends BaseLavaSharkEvent<'playerDestroy'> { await setVoiceChannelStatus(bot, client, player.voiceChannelId, null); } - // Clean up dashboard message + // Clean up dashboard message: the session is over and nothing will + // update it anymore, so delete it instead of leaving a stale message if (player.dashboardMsg) { - await client.dashboard.destroy(player); + await player.dashboardMsg.delete().catch(() => {}); + player.dashboardMsg = null; } } } diff --git a/src/events/lavashark/TrackAddEvent.ts b/src/events/lavashark/TrackAddEvent.ts index 04a23ac4..c287df71 100644 --- a/src/events/lavashark/TrackAddEvent.ts +++ b/src/events/lavashark/TrackAddEvent.ts @@ -33,14 +33,12 @@ export class TrackAddEvent extends BaseLavaSharkEvent<'trackAdd'> { } // Refresh dashboard - try { - if (player.dashboardMsg) await player.dashboardMsg.delete(); - } catch (error) { - bot.logger.error( bot.shardId, 'Dashboard delete error:' + error); + if (player.current) { + if (!player.dashboardMsg) { + await client.dashboard.initialize((player.metadata as Message), player); + } + await client.dashboard.update(player, player.current); } - - await client.dashboard.initialize((player.metadata as Message), player); - await client.dashboard.update(player, player.current!); } /** diff --git a/src/lib/DashboardManager.ts b/src/lib/DashboardManager.ts index 91e25410..e7b1b57c 100644 --- a/src/lib/DashboardManager.ts +++ b/src/lib/DashboardManager.ts @@ -17,6 +17,7 @@ import type { Client } from 'discord.js'; export class DashboardManager { #bot: Bot; #client: Client; + #updatePromise: Promise = Promise.resolve(); constructor(bot: Bot, client: Client) { this.#bot = bot; @@ -24,63 +25,162 @@ export class DashboardManager { } /** - * Initialize dashboard in a channel + * Initialize dashboard in a channel. + * Serialized with updates and destroys so no two dashboard messages + * can ever be created for the same player at the same time. */ - public async initialize(interactionOrMessage: ChatInputCommandInteraction | Message, player: Player): Promise { + public async initialize( + target: ChatInputCommandInteraction | Message | any, + player: Player + ): Promise { + this.#updatePromise = this.#updatePromise + .then(() => this.#performInitialize(target, player)) + .catch(() => {}); + return this.#updatePromise; + } + + async #performInitialize( + target: ChatInputCommandInteraction | Message | any, + player: Player + ): Promise { let channel; - if (interactionOrMessage instanceof Message) { - channel = (interactionOrMessage as Message).channel; + if (target instanceof Message) { + channel = target.channel; + } + else if (target instanceof ChatInputCommandInteraction) { + channel = target.channel; } - else if (interactionOrMessage instanceof ChatInputCommandInteraction) { - channel = (interactionOrMessage as ChatInputCommandInteraction).channel; + else if (target && typeof target.send === 'function') { + channel = target; } else { - throw new TypeError('Invalid Interaction or Message type'); + throw new TypeError('Invalid Interaction, Message, or Channel type'); + } + + if (player.dashboardMsg) { + try { + await player.dashboardMsg.delete(); + } catch (_) {} + player.dashboardMsg = null; } const lng = this.#bot.guildLanguageManager?.get(player.guildId); - player.dashboardMsg = await (channel as any /* discord.js type error ? (v14.16.2) */).send({ + // Remove stale dashboards from earlier sessions (e.g. after a bot + // restart the in-memory reference is gone) so only the new one stays + const dashboardTitle = this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }); + try { + const messages = await (channel as any).messages.fetch({ limit: 30 }); + for (const message of messages.values()) { + if (message.author?.id === this.#client.user?.id && + message.embeds.some((embed: any) => embed.title === dashboardTitle)) { + await message.delete().catch(() => {}); + } + } + } catch (_) {} + + player.dashboardMsg = await (channel as any).send({ embeds: [embeds.connected(this.#bot, lng)], components: [] }); } /** - * Update dashboard with current track and player state + * Update dashboard with current track and player state (smart edit or re-send to bottom) */ public async update(player: Player, track: Track): Promise { - if (!player.dashboardMsg) { - this.#bot.logger.error( this.#bot.shardId, 'Dashboard update called but dashboard is null'); - return; - } + this.#updatePromise = this.#updatePromise.then(() => this.#performUpdate(player, track)).catch(() => {}); + return this.#updatePromise; + } + async #performUpdate(player: Player, track: Track): Promise { const lng = this.#bot.guildLanguageManager?.get(player.guildId); - const subtitle = await this.#buildSubtitle(player, track, lng); + let subtitle = await this.#buildSubtitle(player, track, lng); const buttons = ButtonsBuilder.createDashboardButtons(player, lng); + const safeTitle = track.title.length > 256 + ? track.title.substring(0, 253) + '...' + : track.title; + + if (subtitle.length > 1024) { + subtitle = subtitle.substring(0, 1021) + '...'; + } + + const channel = player.dashboardMsg?.channel + ?? (player.textChannelId ? this.#client.channels.cache.get(player.textChannelId) : null); + + if (!channel || typeof (channel as any).send !== 'function') { + this.#bot.logger.error(this.#bot.shardId, 'Dashboard update called but channel is missing or invalid'); + return; + } + + const embedPayload = { + embeds: [embeds.dashboard( + this.#bot, + this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), + safeTitle, + subtitle, + track.uri, + track.thumbnail! + )], + components: [buttons] + }; + + if (player.dashboardMsg) { + try { + const lastMsgId = (channel as any).lastMessageId; + if (!lastMsgId || lastMsgId === player.dashboardMsg.id) { + await player.dashboardMsg.edit(embedPayload); + return; + } + } catch (_) {} + + // The dashboard is no longer the last message: move it to the + // bottom. A replacement is only sent after the old one was + // actually deleted, otherwise the message is refreshed in place + // and no duplicate can ever appear. + let deleted = false; + try { + await player.dashboardMsg.delete(); + deleted = true; + } catch (_) {} + + if (!deleted) { + try { + await player.dashboardMsg.edit(embedPayload); + return; + } catch (_) { + // Message is gone: drop the reference and send a new one + player.dashboardMsg = null; + } + } + else { + player.dashboardMsg = null; + } + } + try { - await player.dashboardMsg.edit({ - embeds: [embeds.dashboard( - this.#bot, - this.#bot.i18n.t('embeds:DASHBOARD_TITLE', { lng }), - track.title, - subtitle, - track.uri, - track.thumbnail! - )], - components: [buttons] - }); + player.dashboardMsg = await (channel as any).send(embedPayload); } catch (error) { - this.#bot.logger.error( this.#bot.shardId, 'Dashboard update error: ' + error); + this.#bot.logger.error(this.#bot.shardId, 'Dashboard update error: ' + error); } } /** - * Destroy dashboard and show disconnect message + * Destroy dashboard and show disconnect message. + * The message reference is kept so the next initialize() deletes it and + * the next update() reuses it instead of leaving a stale duplicate. + * Serialized with updates so a queued update can never race the destroy. */ public async destroy(player: Player): Promise { + this.#updatePromise = this.#updatePromise + .then(() => this.#performDestroy(player)) + .catch(() => {}); + return this.#updatePromise; + } + + async #performDestroy(player: Player): Promise { if (!player.dashboardMsg) { return; } @@ -95,9 +195,6 @@ export class DashboardManager { } catch (error) { this.#bot.logger.error( this.#bot.shardId, 'Dashboard destroy error: ' + error); } - finally { - player.dashboardMsg = null; - } } /** @@ -106,10 +203,12 @@ export class DashboardManager { async #buildSubtitle(player: Player, track: Track, lng?: string): Promise { const repeatModeLabel = this.#getRepeatModeLabel(player.repeatMode, lng); + const currentVolume = player.volume ?? (player.setting as any)?.volume ?? this.#bot.guildVolumeManager?.get(player.guildId) ?? this.#bot.config.bot.volume.default; + let subtitle = this.#bot.i18n.t('embeds:DASHBOARD_SUBTITLE', { author: track.author, duration: track.duration.label, - volume: player.volume, + volume: currentVolume, repeatMode: repeatModeLabel, lng }); diff --git a/src/lib/DatabaseManager.ts b/src/lib/DatabaseManager.ts index 9c13d4b2..6eec5c66 100644 --- a/src/lib/DatabaseManager.ts +++ b/src/lib/DatabaseManager.ts @@ -59,6 +59,10 @@ export class DatabaseManager { return; } + try { + this.db.pragma('wal_checkpoint(TRUNCATE)'); + } catch (_) {} + this.db.close(); this.db = null; this.bot.logger.log(this.bot.shardId, '[DatabaseManager] Database connection closed.'); @@ -71,6 +75,7 @@ export class DatabaseManager { this.db.pragma('journal_mode = WAL'); // Default is DELETE this.db.pragma('synchronous = NORMAL'); // Default is FULL + this.db.pragma('temp_store = MEMORY'); this.db.pragma('foreign_keys = ON'); // Default is OFF this.db.pragma('busy_timeout = 5000'); // Default is 0 (no timeout) } @@ -113,6 +118,7 @@ export class DatabaseManager { guild_id TEXT NOT NULL, name TEXT NOT NULL, created_at INTEGER NOT NULL, + is_m3u INTEGER DEFAULT 0, UNIQUE(guild_id, name) ); @@ -128,5 +134,12 @@ export class DatabaseManager { FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE ); `); + + const columns = this.db.prepare('PRAGMA table_info(playlists)').all() as Array<{ name: string }>; + const hasIsM3u = columns.some((col) => col.name === 'is_m3u'); + + if (!hasIsM3u) { + this.db.exec('ALTER TABLE playlists ADD COLUMN is_m3u INTEGER DEFAULT 0;'); + } } } diff --git a/src/lib/PlaylistManager.ts b/src/lib/PlaylistManager.ts index d2b077ac..f8e59541 100644 --- a/src/lib/PlaylistManager.ts +++ b/src/lib/PlaylistManager.ts @@ -15,11 +15,14 @@ export interface PlaylistTrack { position: number; } + + export interface Playlist { id: number; guildId: string; name: string; createdAt: number; + isM3u?: boolean; tracks?: PlaylistTrack[]; } @@ -61,21 +64,29 @@ export class PlaylistManager { try { const playlistRow = db.prepare(` - SELECT id, guild_id as guildId, name, created_at as createdAt + SELECT id, guild_id as guildId, name, created_at as createdAt, is_m3u as isM3u FROM playlists - WHERE guild_id = ? AND name = ? - `).get(guildId, name) as Playlist | undefined; + WHERE guild_id = ? AND name = ? COLLATE NOCASE + `).get(guildId, name) as any; if (!playlistRow) return null; + const playlist: Playlist = { + id: Number(playlistRow.id), + guildId: String(playlistRow.guildId), + name: String(playlistRow.name), + createdAt: Number(playlistRow.createdAt), + isM3u: Boolean(playlistRow.isM3u), + }; + const tracks = db.prepare(` SELECT id, playlist_id as playlistId, title, url, encoded, author, duration, position FROM playlist_tracks WHERE playlist_id = ? ORDER BY position ASC - `).all(playlistRow.id) as any[]; + `).all(playlist.id) as any[]; - playlistRow.tracks = tracks.map(t => ({ + playlist.tracks = tracks.map(t => ({ id: t.id, playlistId: t.playlistId, title: t.title, @@ -86,7 +97,7 @@ export class PlaylistManager { position: t.position })); - return playlistRow; + return playlist; } catch (error) { this.bot.logger.error(this.bot.shardId, `[PlaylistManager] Error getting playlist ${name}: ${error}`); return null; @@ -134,7 +145,7 @@ export class PlaylistManager { } } - public saveCurrentQueue(guildId: string, name: string, tracks: Omit[]): boolean { + public saveCurrentQueue(guildId: string, name: string, tracks: Omit[], isM3u: boolean = false): boolean { const db = this.db; if (!db || tracks.length === 0 || tracks.length > PLAYLIST_TRACK_LIMIT) return false; @@ -145,12 +156,12 @@ export class PlaylistManager { `); const replacePlaylist = db.transaction(() => { - db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ?').run(guildId, name); + db.prepare('DELETE FROM playlists WHERE guild_id = ? AND name = ? COLLATE NOCASE').run(guildId, name); const playlistInfo = db.prepare(` - INSERT INTO playlists (guild_id, name, created_at) - VALUES (?, ?, ?) - `).run(guildId, name, Date.now()); + INSERT INTO playlists (guild_id, name, created_at, is_m3u) + VALUES (?, ?, ?, ?) + `).run(guildId, name, Date.now(), isM3u ? 1 : 0); const playlistId = Number(playlistInfo.lastInsertRowid); for (let i = 0; i < tracks.length; i++) { @@ -255,7 +266,7 @@ export class PlaylistManager { author: null })); - const success = this.saveCurrentQueue(guildId, name, playlistTracks); + const success = this.saveCurrentQueue(guildId, name, playlistTracks, true); return success ? {success: true, trackCount: playlistTracks.length} : {success: false, reason: 'SAVE_FAILED'}; diff --git a/src/lib/QueuePersistence.ts b/src/lib/QueuePersistence.ts index 92c4fc13..ea2c0ef3 100644 --- a/src/lib/QueuePersistence.ts +++ b/src/lib/QueuePersistence.ts @@ -3,6 +3,12 @@ import { ChannelType } from 'discord.js'; import type { Client } from 'discord.js'; import type { Player, Track } from 'lavashark'; import type { Bot, QueueTableRow } from '../@types/index.js'; +import { + decodeTracksWithRetry, + decodeTrackWithRetry, + searchWithRetry, + sleep, +} from '../utils/functions/lavasharkRequest.js'; /** @@ -40,6 +46,27 @@ interface SerializedTrack { requesterTag: string; } +/** + * Format milliseconds as a duration label (mirrors lavashark's formatTime) + */ +const formatDurationLabel = (milliseconds: number): string => { + const seconds = Math.floor(milliseconds / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + let timeString = ''; + if (days > 0) { + timeString += `${days}d `; + } + if (hours % 24 > 0) { + timeString += `${(hours % 24).toString().padStart(2, '0')}:`; + } + timeString += `${(minutes % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`; + + return timeString; +}; + /** * Manager for persisting queue state to SQLite database */ @@ -87,18 +114,19 @@ export class QueuePersistence { } const serializeTrack = (track: Track): SerializedTrack | null => { - if (!('encoded' in track)) return null; + if (!track) return null; + const encodedTrack = ('encoded' in track && typeof track.encoded === 'string') ? track.encoded : ((track as any).track ?? ''); return { - track: track.encoded, + track: encodedTrack, info: { - identifier: track.identifier, - title: track.title, - author: track.author, + identifier: track.identifier || '', + title: track.title || '', + author: track.author || '', length: typeof track.duration === 'number' ? track.duration : 0, - uri: track.uri, + uri: track.uri || '', sourceName: (track as any).sourceName || 'youtube', - isSeekable: track.isSeekable, - isStream: track.isStream + isSeekable: track.isSeekable ?? true, + isStream: track.isStream ?? false }, requesterId: track.requester?.id || '', requesterTag: track.requester?.tag || '' @@ -115,19 +143,19 @@ export class QueuePersistence { // Append queue tracks for (const track of player.queue.tracks) { - if ('encoded' in track) { - const s = serializeTrack(track as Track); - if (s) serializedTracks.push(s); - } + const s = serializeTrack(track as Track); + if (s) serializedTracks.push(s); } + const currentVolume = player.volume ?? (player.setting as any)?.volume ?? this.bot.guildVolumeManager?.get(player.guildId) ?? this.bot.config.bot.volume.default; + const queueData: PersistedQueue = { guildId: player.guildId, voiceChannelId: player.voiceChannelId || '', textChannelId: player.textChannelId || '', tracks: serializedTracks, currentTrackIndex: 0, - volume: (player as any).volume || 100, + volume: currentVolume, repeatMode: player.repeatMode, paused: player.paused, position: player.position, @@ -290,49 +318,51 @@ export class QueuePersistence { } } - // Restore tracks - try encoded track string first, fall back to URI search - for (const serializedTrack of queueData.tracks) { - try { - let result = null; - - // Try loading by encoded track string first - try { - result = await client.lavashark.search(serializedTrack.track); - } catch (_) { - // Encoded string failed, will try fallback - } + // Restore tracks in batch chunks with bulk decode and retry + const TRACK_REQUEST_DELAY_MS = 250; + const serializedEntries = queueData.tracks.map((track, index) => ({ track, index })); + const encodedEntries = serializedEntries.filter( + ({ track }) => track.track && track.track.trim() !== '', + ); - // Fallback: search by URI - if (!result || !result.tracks || result.tracks.length === 0) { - try { - result = await client.lavashark.search(serializedTrack.info.uri); - } catch (_) { - // URI search failed, will try title search + const restoredByIndex = new Map(); + + if (encodedEntries.length > 0) { + const bulkDecoded = await decodeTracksWithRetry( + client.lavashark, + encodedEntries.map(({ track }) => track.track), + ); + + if (bulkDecoded && bulkDecoded.length === encodedEntries.length) { + for (let i = 0; i < encodedEntries.length; i++) { + const track = bulkDecoded[i]; + if (track) { + restoredByIndex.set( + encodedEntries[i].index, + this.applySerializedMetadata(track, encodedEntries[i].track), + ); } } + } + } - // Final fallback: search by title + author - if (!result || !result.tracks || result.tracks.length === 0) { - try { - result = await client.lavashark.search(`ytsearch:${serializedTrack.info.title} ${serializedTrack.info.author}`); - } catch (_) { - // All methods failed - } - } + const allRestoredTracks: any[] = []; + for (const { track, index } of serializedEntries) { + const restored = restoredByIndex.get(index); + if (restored) { + allRestoredTracks.push(restored); + continue; + } - if (result && result.tracks && result.tracks.length > 0) { - const track = result.tracks[0]; - track.requester = { - id: serializedTrack.requesterId, - tag: serializedTrack.requesterTag - } as any; - player.queue.add(track); - } else { - this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); - } - } catch (error) { - this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); + const fallback = await this.restoreTrack(client, track); + if (fallback) { + allRestoredTracks.push(fallback); } + await sleep(TRACK_REQUEST_DELAY_MS); + } + + for (const track of allRestoredTracks) { + player.queue.add(track); } // Restore settings @@ -340,11 +370,20 @@ export class QueuePersistence { // Connect and play await player.connect(); - if (!player.playing && !player.paused) { + if (!player.playing && !player.paused && player.queue.tracks.length > 0) { await player.play(); } - // Set volume after playback starts (Player.volume is a read-only getter) + if (queueData.paused && player.playing) { + await player.pause(); + } + + // Set volume after playback starts + if (!player.setting) { + player.setting = { queuePage: null, volume: queueData.volume, fairQueueRotation: [] }; + } else { + player.setting.volume = queueData.volume; + } player.filters.setVolume(queueData.volume); // Seek to saved position after playback starts @@ -368,6 +407,80 @@ export class QueuePersistence { } } + /** + * Resolve a single serialized track back into a playable track. + * Decodes the saved encoded string first (exact restore), then falls back + * to searching by URI, then by title + author. Requests are retried once + * each because Lavalink nodes rate-limit REST calls (HTTP 429). + */ + private async restoreTrack( + client: Client, + serializedTrack: SerializedTrack, + ): Promise { + try { + let resolvedTrack: any = null; + + // 1) Decode the saved encoded track exactly (preserves the original track data) + if (serializedTrack.track && serializedTrack.track.trim() !== '') { + resolvedTrack = await decodeTrackWithRetry( + client.lavashark, + serializedTrack.track, + ); + } + + // 2) Fallback: search by URI + if (!resolvedTrack && serializedTrack.info?.uri && serializedTrack.info.uri.trim() !== '') { + const result = await searchWithRetry(client.lavashark, serializedTrack.info.uri); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + resolvedTrack = result.tracks[0]; + } + } + + // 3) Final fallback: search by title + author + if (!resolvedTrack && serializedTrack.info?.title && serializedTrack.info.title.trim() !== '') { + const query = serializedTrack.info.author + ? `${serializedTrack.info.title} ${serializedTrack.info.author}`.trim() + : serializedTrack.info.title.trim(); + const result = await searchWithRetry(client.lavashark, `ytsearch:${query}`); + if (result && Array.isArray(result.tracks) && result.tracks.length > 0) { + resolvedTrack = result.tracks[0]; + } + } + + if (resolvedTrack) { + return this.applySerializedMetadata(resolvedTrack, serializedTrack); + } + + this.bot.logger.log(this.bot.shardId, `[QueuePersistence] Could not restore track "${serializedTrack.info.title}", skipping`); + } catch (error) { + this.bot.logger.error(this.bot.shardId, `[QueuePersistence] Failed to restore track ${serializedTrack.info.title}: ${error}`); + } + return null; + } + + /** + * Restore the originally saved metadata so names are never replaced by + * whatever the fallback search returned + */ + private applySerializedMetadata(track: any, serializedTrack: SerializedTrack): any { + const info = serializedTrack.info ?? {}; + if (info.title) track.title = info.title; + if (info.author) track.author = info.author; + if (info.uri) track.uri = info.uri; + if (info.identifier) track.identifier = info.identifier; + if (typeof info.length === 'number' && info.length > 0) { + track.duration = { + label: formatDurationLabel(info.length), + value: info.length, + }; + } + track.requester = { + id: serializedTrack.requesterId, + tag: serializedTrack.requesterTag + } as any; + return track; + } + /** * Check if any persisted queues exist for a specific voice channel * @param voiceChannelId - Voice channel ID to check diff --git a/src/lib/api/routes/AuthRouter.ts b/src/lib/api/routes/AuthRouter.ts index c56667b0..77f5e343 100644 --- a/src/lib/api/routes/AuthRouter.ts +++ b/src/lib/api/routes/AuthRouter.ts @@ -5,7 +5,7 @@ import { parseCookie } from 'cookie'; import undici from 'undici'; import { LoginTypeEnum } from '../../../@types/index.js'; -import { hashGenerator } from '../../hashGenerator.js'; +import { generateRandomKey } from '../../../utils/functions/generateRandomKey.js'; import { loginSchema } from '../validators/schemas.js'; import { noContent, ok, problem } from '../http.js'; import { validateBody } from '../validators/validate.js'; @@ -126,7 +126,7 @@ export class AuthRouter extends BaseRouter { if (username === siteConfig.user.username && passwordMatch) { this.sessionManager.unblockIP(userIP); - const sessionId = hashGenerator.generateRandomKey(); + const sessionId = generateRandomKey(); this.sessionManager.createSession(sessionId); res.cookie('sessionId', sessionId, SESSION_COOKIE_OPTIONS); @@ -245,7 +245,7 @@ export class AuthRouter extends BaseRouter { }); } - const state = hashGenerator.generateRandomKey(); + const state = generateRandomKey(); this.#oauthStates.set(state, { expiresAt: Date.now() + OAUTH2_STATE_TTL_MS, requesterIp, @@ -344,7 +344,7 @@ export class AuthRouter extends BaseRouter { if (this.bot.config.bot.admin.includes(user.id)) { this.sessionManager.unblockIP(userIP); - const sessionId = hashGenerator.generateRandomKey(); + const sessionId = generateRandomKey(); this.sessionManager.createSession(sessionId); res.cookie('sessionId', sessionId, SESSION_COOKIE_OPTIONS); diff --git a/src/lib/handlers/NodeSelectHandler.ts b/src/lib/handlers/NodeSelectHandler.ts new file mode 100644 index 00000000..16eb3e50 --- /dev/null +++ b/src/lib/handlers/NodeSelectHandler.ts @@ -0,0 +1,76 @@ +import { ActionRowBuilder, MessageFlags, StringSelectMenuBuilder } from 'discord.js'; +import { NodeState } from 'lavashark'; + +import { embeds } from '../../embeds/index.js'; + +import type { Client, StringSelectMenuInteraction } from 'discord.js'; +import type { Bot } from '../../@types/index.js'; + +export class NodeSelectHandler { + public static async handle( + bot: Bot, + client: Client, + interaction: StringSelectMenuInteraction + ): Promise { + if (interaction.customId !== 'select_node') return; + + const lng = bot.guildLanguageManager?.get(interaction.guildId!) || bot.config.bot.i18n.defaultLocale; + const nodeName = interaction.values[0]; + const nodes = client.lavashark.nodes; + + const targetNode = nodes.find(node => node.identifier === nodeName); + + if (!targetNode) { + await interaction.reply({ + embeds: [embeds.validNodeName(bot, nodes.map(n => `\`${n.identifier}\``).join('\n'), lng)], + flags: MessageFlags.Ephemeral + }); + return; + } + + if (targetNode.state !== NodeState.CONNECTED) { + await interaction.reply({ + embeds: [embeds.nodeDisconnected(bot, nodeName, lng)], + flags: MessageFlags.Ephemeral + }); + return; + } + + await interaction.deferUpdate(); + + try { + const [nodeInfo, nodeStats, nodePing] = await Promise.all([ + targetNode.getInfo(), + targetNode.getStats(), + client.lavashark.nodePing(targetNode) + ]); + + const selectOptions = nodes.slice(0, 25).map(node => { + const isConnected = node.state === NodeState.CONNECTED; + return { + label: node.identifier, + value: node.identifier, + default: node.identifier === nodeName, + description: isConnected + ? (bot.i18n.t('embeds:NODE_STATUS_PING', { lng }) + `: ${nodePing}ms`) + : bot.i18n.t('embeds:NODE_DISCONNECTED', { lng }).replace(/\*/g, ''), + emoji: isConnected ? '✅' : '❌' + }; + }); + + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('select_node') + .setPlaceholder(bot.i18n.t('commands:CONFIG_NODE_OPTION_DESCRIPTION', { lng })) + .addOptions(selectOptions); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + await interaction.editReply({ + embeds: [embeds.nodeStatus(bot, nodeName, nodeInfo, nodeStats, nodePing, lng)], + components: [row] + }); + } catch (error) { + bot.logger.error(bot.shardId, '[NodeSelectHandler] Error fetching node status: ' + error); + } + } +} diff --git a/src/lib/handlers/SkipButtonHandler.ts b/src/lib/handlers/SkipButtonHandler.ts index 630ba825..a2883947 100644 --- a/src/lib/handlers/SkipButtonHandler.ts +++ b/src/lib/handlers/SkipButtonHandler.ts @@ -1,7 +1,6 @@ import { RepeatMode } from 'lavashark'; import { MessageFlags } from 'discord.js'; import { DashboardButtonHandler } from './DashboardButtonHandler.js'; -import { ButtonsBuilder } from '../builders/ButtonsBuilder.js'; import { PermissionManager } from '../PermissionManager.js'; import { embeds } from '../../embeds/index.js'; @@ -57,12 +56,10 @@ export class SkipButtonHandler extends DashboardButtonHandler { if (player.repeatMode === RepeatMode.TRACK) { player.setRepeatMode(RepeatMode.OFF); - await player.skip(); - player.setRepeatMode(RepeatMode.TRACK); - } else { - await player.skip(); } + await player.skip(); + if (!hasMoreTracks) { try { await interaction.reply({ @@ -73,7 +70,8 @@ export class SkipButtonHandler extends DashboardButtonHandler { } catch (_) {} } - const row = ButtonsBuilder.createDashboardButtons(player, lng); - await interaction.update({ components: [row] }); + if (!interaction.replied && !interaction.deferred) { + await interaction.deferUpdate(); + } } } diff --git a/src/lib/hashGenerator.ts b/src/lib/hashGenerator.ts deleted file mode 100644 index 1385744d..00000000 --- a/src/lib/hashGenerator.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createHash, randomBytes } from 'crypto'; - - -const hashGenerator = { - /** - * Generates a random 32-byte key in hexadecimal format. - * @returns {string} - A randomly generated key. - */ - generateRandomKey: (): string => { - return randomBytes(32).toString('hex'); - }, - - /** - * Generates a hash using the SHA-512 algorithm - * and a provided password along with a pre-generated salt. - * @param {string} password - The password to be hashed. - * @returns {string} - The hashed password. - */ - generateHash: (password: string): string => { - return createHash('sha512') - .update(password + salt, 'utf-8') - .digest('hex'); - } -}; - -export { hashGenerator }; - - -// pre-generated salt -const salt = hashGenerator.generateRandomKey(); diff --git a/src/locales/en-US/commands.json b/src/locales/en-US/commands.json index c78aaf74..4eeff197 100644 --- a/src/locales/en-US/commands.json +++ b/src/locales/en-US/commands.json @@ -6,13 +6,13 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Remove a user from the blacklist", "CONFIG_BLACKLIST_OPTION_LIST": "List all blacklisted users", "CONFIG_BLACKLIST_OPTION_USER": "User to add or remove from blacklist", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not in the dynamic blacklist.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dynamic Blacklist", - "MESSAGE_BLACKLIST_LIST_EMPTY": "The dynamic blacklist is empty.", - "CONFIG_CLEAR_DESCRIPTION": "Clear all the music in the queue", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> has been added to the blacklist", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> has been removed from the blacklist", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> is already blacklisted", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> is not on the blacklist", + "MESSAGE_BLACKLIST_LIST_TITLE": "Current Blacklist", + "MESSAGE_BLACKLIST_LIST_EMPTY": "The blacklist is currently empty", + "CONFIG_CLEAR_DESCRIPTION": "Clear the music queue", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Move the dashboard to the bottom", "CONFIG_DASHBOARD_USAGE": "dashboard", @@ -21,25 +21,25 @@ "CONFIG_DJ_OPTION_DESCRIPTION": "The user to add as DJ (optional)", "CONFIG_FILTER_DESCRIPTION": "Set music filter", "CONFIG_FILTER_USAGE": "filter [effect name]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "Select the music filter mode you want to set", - "CONFIG_HELP_DESCRIPTION": "Get commands help", + "CONFIG_FILTER_OPTION_DESCRIPTION": "Select the music filter to apply", + "CONFIG_HELP_DESCRIPTION": "Get help with commands", "CONFIG_HELP_USAGE": "help [command]", - "CONFIG_HELP_OPTION_DESCRIPTION": "which command need help", + "CONFIG_HELP_OPTION_DESCRIPTION": "The command you need help with", "CONFIG_JOIN_DESCRIPTION": "Join current voice channel", "CONFIG_JOIN_USAGE": "join", - "CONFIG_LANG_DESCRIPTION": "Change the Bot locale", - "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Locale name", + "CONFIG_LANG_DESCRIPTION": "Change the bot's language", + "CONFIG_LANG_USAGE": "language ", + "CONFIG_LANG_OPTION_DESCRIPTION": "Select a locale", "CONFIG_LEAVE_DESCRIPTION": "Leave current voice channel", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "Turns the music loop mode on or off", + "CONFIG_LOOP_DESCRIPTION": "Turn the music loop mode on or off", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Music loop mode", "CONFIG_MOVE_DESCRIPTION": "Swap the positions of two songs in the queue", "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "To swap song position 1", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "To swap song position 2", - "CONFIG_NODE_DESCRIPTION": "Show nodes active status", + "CONFIG_MOVE_OPTION_DESCRIPTION": "Track position 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Track position 2", + "CONFIG_NODE_DESCRIPTION": "Show the status of active nodes", "CONFIG_NODE_USAGE": "node [node name]", "CONFIG_NODE_OPTION_DESCRIPTION": "The node name", "CONFIG_NOW_PLAYING_DESCRIPTION": "Show currently playing music", @@ -48,40 +48,40 @@ "CONFIG_PAUSE_USAGE": "pause", "CONFIG_PING_DESCRIPTION": "Get server ping", "CONFIG_PING_USAGE": "ping", - "CONFIG_PLAY_DESCRIPTION": "Enter your song link or song name to play", + "CONFIG_PLAY_DESCRIPTION": "Enter a song link or name to play", "CONFIG_PLAY_USAGE": "play ", "CONFIG_PLAY_OPTION_DESCRIPTION": "The song link or song name", - "CONFIG_PLAYFIRST_DESCRIPTION": "Enter your song link or song name to play first", + "CONFIG_PLAYFIRST_DESCRIPTION": "Enter a song link or name to play first", "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "The song link or song name to play first", "CONFIG_PLAYLAST_DESCRIPTION": "Replay the current or last played song", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Re-queued the song.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ This command can only be used when the queue is empty", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ No recently played song found", + "MESSAGE_PLAYLAST_SUCCESS": "✅ The track is back in the queue", "CONFIG_QUEUE_DESCRIPTION": "Show current playlist", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Select a song to remove from the playlist", - "CONFIG_REMOVE_USAGE": "remove [from index to index 2 track]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "track index number", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "from index to index 2 track", + "CONFIG_REMOVE_USAGE": "remove [from to]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Track number", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Track range", "CONFIG_RESUME_DESCRIPTION": "Resume paused track", "CONFIG_RESUME_USAGE": "resume", - "CONFIG_SEARCH_DESCRIPTION": "Enter song name to search", + "CONFIG_SEARCH_DESCRIPTION": "Enter a song name to search", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "The song name", "CONFIG_SEEK_DESCRIPTION": "Seeks to a certain time in the track", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "traget time (ex: 3m20s, 1:20:55)", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Target time (ex: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Show currently active servers", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Shuffle playlist", "CONFIG_SHUFFLE_USAGE": "shuffle", - "CONFIG_SKIP_DESCRIPTION": "Skip currnet track", + "CONFIG_SKIP_DESCRIPTION": "Skip current track", "CONFIG_SKIP_USAGE": "skip", "CONFIG_STATUS_DESCRIPTION": "Show the bot status", "CONFIG_STATUS_USAGE": "status", - "CONFIG_STOP_DESCRIPTION": "Stop the currently playing music and clear the queue, but don't leave the channel", + "CONFIG_STOP_DESCRIPTION": "Stop playback and clear the queue without leaving the channel", "CONFIG_STOP_USAGE": "stop", "CONFIG_VOLUME_DESCRIPTION": "Change bot volume", "CONFIG_VOLUME_USAGE": "v <0-100>", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [volume]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "The default volume to set (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Command error. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ There is currently no music to be play in the queue.", - "ERROR_NO_PLAYING": "❌ There is no music currently playing.", - "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if enabled) can also skip.", - "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if enabled) can also seek.", - "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if enabled) can also pause.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join voice channel.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ There are no tracks in the queue right now", + "ERROR_NO_PLAYING": "❌ There is no music currently playing", + "ERROR_SKIP_NOT_REQUESTER": "❌ Only the person who requested this song can skip it. Admins and DJs (if present) can also skip.", + "ERROR_SEEK_NOT_REQUESTER": "❌ Only the person who requested this song can seek. Admins and DJs (if present) can also seek.", + "ERROR_PAUSE_NOT_REQUESTER": "❌ Only the person who requested this song can pause it. Admins and DJs (if present) can also pause.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ I can't join the voice channel", "ERROR_PLAY_MUSIC": "❌ The service is experiencing some problems, please try again. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ No results found. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Time expired.", - "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated.", - "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs.", - "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs.", - "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ.", - "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ.", - "MESSAGE_DJ_STATIC_MODE": "DJ permissions are managed through configuration in STATIC mode.", - "MESSAGE_DJ_NO_PLAYER": "No active music player found.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ.", - "MESSAGE_DJ_SUCCESS": "Successfully added <@{{userId}}> as DJ.", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired", + "MESSAGE_CLEAR_SUCCESS": "✅ Queue cleared", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Dashboard updated", + "MESSAGE_DJ_NO_PERMISSION": "You need DJ or Admin permissions to add DJs", + "MESSAGE_DJ_ADMIN_ONLY": "Only administrators can add or remove DJs", + "MESSAGE_DJ_MENTION_USER": "Please mention a user to add as DJ", + "MESSAGE_DJ_NO_BOTS": "Cannot add bots as DJ", + "MESSAGE_DJ_STATIC_MODE": "In STATIC mode, DJ permissions can only be set through the config file", + "MESSAGE_DJ_NO_PLAYER": "There's no player currently playing music", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> is already a DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> has been added as a DJ", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ left the channel! Auto-selected new DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ List", "MESSAGE_DJ_LIST_ADMINS": "Admins", "MESSAGE_DJ_LIST_ROLE_DJS": "Role-based DJs", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dynamic DJs", - "MESSAGE_DJ_LIST_NONE": "No DJs configured.", + "MESSAGE_DJ_LIST_NONE": "No DJs configured", "MESSAGE_DJ_LIST_ERROR": "❌ Failed to load DJ list. Please try again.", - "MESSAGE_DJ_ROLE_NOT_SET": "No DJ role configured", - "MESSAGE_FILTER_NOT_FOUND": "❌ The effect name not found.", - "MESSAGE_FILTER_SELECT_LIST": "Select the music filter mode you want to set. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Select filter mode", - "MESSAGE_HELP_NOT_FOUND": "❌ The command not found.", + "MESSAGE_DJ_ROLE_NOT_SET": "No one has been assigneed to a permanent DJ role", + "MESSAGE_FILTER_NOT_FOUND": "❌ Effect not found", + "MESSAGE_FILTER_SELECT_LIST": "Select a music filter: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Select an effect", + "MESSAGE_HELP_NOT_FOUND": "❌ Command not found", "MESSAGE_HELP_SELECT_LIST": "Choose a command to get help. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Select the help", - "MESSAGE_JOIN_SUCCESS": "✅ Bot Joined.", - "MESSAGE_LANG_ARGS_ERROR": "The locale parameter is wrong, currently available languages: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Currently available languages: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Changed locale to `{{locale}}` successfully", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot left.", + "MESSAGE_HELP_SELECT_MODE": "Select a command", + "MESSAGE_JOIN_SUCCESS": "✅ Joined the voice channel", + "MESSAGE_LANG_ARGS_ERROR": "Invalid locale code. Available languages: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Available languages: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Locale changed to `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Left the voice channel", "REPEAT_MODE_OFF": "OFF", "REPEAT_MODE_SINGLE": "SINGLE", "REPEAT_MODE_ALL": "ALL", @@ -136,49 +136,49 @@ "LOOP_CHOICE_ALL": "All", "LOOP_SELECT_PLACEHOLDER": "Select the loop mode", "LOOP_SELECT_DESCRIPTION": "Set loop mode to {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Set loop to `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ The song order was swapped successfully.", - "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter a valid index range. (1 - {{max}})", + "MESSAGE_LOOP_MODE": "✅ Loop mode is now `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Track order changed", + "MESSAGE_MOVE_WRONG_INDEX": "❌ Please enter positions in a valid range. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nand {{length}} other songs", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Save Song", "MESSAGE_NOW_PLAYING_SUBTITLE": "Author: **{{author}}**\nDuration: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Now Playing: {{title}} | Requested by {{requester}}", "UNKNOWN_USER": "Unknown", - "MESSAGE_PAUSE_FAIL": "❌ Music pause failed.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Write the name of the music you want to search.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Music added.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches.", + "MESSAGE_PAUSE_FAIL": "❌ Failed to pause the music", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ The music has been paused", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Music paused", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Please provide a song name or link", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Track added to the queue", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ No matches found", "MESSAGE_PLAY_SELECT_DURATION": "Duration: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Select the music", - "MESSAGE_REMOVE_CANCEL": "✅ Cancelled remove.", - "MESSAGE_REMOVE_FAIL": "❌ Music remove failed.", - "MESSAGE_REMOVE_INSTRUCTION": "Choose a song from **1** to **{{length}}** to **remove** or enter others to cancel selection. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ No music in queue after current.", - "MESSAGE_RESUME_FAIL": "❌ Music resume failed.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ The music has been resumed.", - "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed.", + "MESSAGE_REMOVE_CANCEL": "✅ Removal cancelled", + "MESSAGE_REMOVE_FAIL": "❌ Failed to remove the track", + "MESSAGE_REMOVE_INSTRUCTION": "Choose a track number from **1** to **{{length}}** to **remove** it, or enter any other number to cancel. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ The queue is empty after the current track", + "MESSAGE_RESUME_FAIL": "❌ Failed to resume the music", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ The music has been resumed", + "MESSAGE_RESUME_SUCCESS": "▶️ Music resumed", "MESSAGE_SEEK_ARGS_ERROR": "❌ Invalid format for the target time.\n(**`ex: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ The seek position is beyond the duration of this track. (`{{duration}}`)\nSkipping to the next song.", - "MESSAGE_SEEK_SUCCESS": "✅ Music has been seeked to `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Music shuffled.", - "MESSAGE_SKIP_FAIL": "❌ Music skip failed.", - "MESSAGE_SKIP_SUCCESS": "✅ Music skipped.", - "MESSAGE_STOP_SUCCESS": "✅ Music stopped.", - "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, with `1` to `{{maxVolume}}` Type a number between.**", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Type a number from `1` to `{{maxVolume}}` to change the volume .**", - "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\\n**Please select the volume you want to set:**", - "MESSAGE_VOLUME_SAME": "❌ The volume you want to change is already the current volume.", + "MESSAGE_SEEK_SKIP": "✅ The requested time (`{{duration}}`) is beyond the track's duration.\nSkipping to the next track.", + "MESSAGE_SEEK_SUCCESS": "✅ Seeked to `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Playlist shuffled", + "MESSAGE_SKIP_FAIL": "❌ Failed to skip the track", + "MESSAGE_SKIP_SUCCESS": "✅ Track skipped", + "MESSAGE_STOP_SUCCESS": "✅ Playback stopped and the queue cleared", + "MESSAGE_VOLUME_ARGS_ERROR": "Current volume: **{{volume}}** 🔊\n**To change the volume, enter a number from `1` to `{{maxVolume}}`.**", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Enter a number from `1` to `{{maxVolume}}` to change the volume.**", + "MESSAGE_VOLUME_SELECT": "Current volume: **{{volume}}%** 🔊\n**Select the volume you want:**", + "MESSAGE_VOLUME_SAME": "❌ That volume is already set", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", - "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume has been changed to **{{volume}}%** for this server! (Max: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume.", + "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Default volume for this server changed to **{{volume}}%**! (Max: {{maxVolume}}%)", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Current default volume: **{{volume}}%** 🔊\nTo change, type a number between `1` and `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Type a number from `1` to `{{maxVolume}}` to change the default volume", "ERROR_QUEUE_LIMIT_REACHED": "❌ You have reached your queue limit! You currently have **{{current}}** songs in the queue (limit: **{{limit}}**). Please wait for some songs to finish before adding more.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ The global queue limit has been reached! The queue currently has **{{total}}** songs (global limit: **{{limit}}**). Please wait for some songs to finish before adding more.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to your queue limit (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to the global queue limit (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to your queue limit (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Playlist partially added: **{{added}}** songs added, **{{skipped}}** songs skipped due to the global queue limit (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "Guild ID", "MESSAGE_SERVER_GUILD": "Guild", "MESSAGE_SERVER_MEMBERS": "Members", @@ -197,73 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Blacklist manager is not initialized.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "The blacklist isn't loaded yet. Please try again later.", "HELP_SELECT_MUSIC_PLACEHOLDER": "Select a Music command", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Utility command", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Select a Service command", "HELP_COMMAND_ALIASES": "Aliases: {{aliases}}", "HELP_COMMAND_NONE": "None", "LABEL_FILTER_CLEAR": "Clear filters", - "CONFIG_PLAYLIST_DESCRIPTION": "Save, load, edit playlists or import them from M3U url.", + "CONFIG_PLAYLIST_DESCRIPTION": "Manage playlists: save, load, delete, and import from M3U", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "List all playlists in the server.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Show the tracks in a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Delete a playlist.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Import a playlist from a remote M3U file.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Remove a track from a playlist.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Save the current queue as a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Load and play a saved playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "List all playlists in the server", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Show the tracks in a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Delete a playlist", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Import a playlist from a remote M3U file", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Remove a track from a playlist", "CONFIG_PLAYLIST_OPTION_NAME": "Playlist name", "CONFIG_PLAYLIST_OPTION_SEARCH": "Specific track title to search in the playlist (Optional)", "CONFIG_PLAYLIST_OPTION_URL": "M3U file download link", "CONFIG_PLAYLIST_OPTION_INDEX": "Track index to remove (starts from 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlist manager is not initialized.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Please specify a playlist name.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download M3U file. (HTTP status: {{status}})", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Playlists aren't available yet. Please try again later.", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Please enter a valid subcommand (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Unknown subcommand", + "ERROR_PLAYLIST_NO_PLAYING": "❌ No music is playing or queue is empty", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Please specify a playlist name", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Failed to save the playlist", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Playlist `{{name}}` not found or it is empty", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Track `{{searchQuery}}` not found in playlist `{{name}}`", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Failed to download the M3U file (error code {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ The M3U download was blocked by the security policy. Use a public HTTPS URL up to {{maxSizeMiB}} MiB that responds within {{timeoutSeconds}} seconds.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ A playlist can contain at most {{limit}} tracks", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Failed to parse and save M3U playlist. Check if it's formatted correctly.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Failed to remove track. Check if it is a valid index.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded. (Success: {{added}}, Skipped: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Saved {{count}} tracks from current queue to playlist `{{name}}`", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Searching and adding {{count}} tracks from playlist `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Playlist `{{name}}` loaded: {{added}} added, {{skipped}} skipped", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ No playlists saved in this server", "MESSAGE_PLAYLIST_LIST_TITLE": "Server Playlists", "MESSAGE_PLAYLIST_INFO_TITLE": "Playlist Info: {{name}} ({{count}} tracks)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*and {{remaining}} more tracks.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Deleted playlist `{{name}}`.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Deleted playlist `{{name}}`", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ External M3U playlist imported!\nRegistered {{count}} tracks in `{{name}}` playlist.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Removed track #{{index}} from playlist `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Search and play a specific genre/channel from imported radio playlist.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Removed track #{{index}} from playlist `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Search and play a specific genre/channel from imported radio playlist", "CONFIG_RADIO_USAGE": "radio [channel]", "CONFIG_RADIO_OPTION_PLAYLIST": "Playlist name to search", "CONFIG_RADIO_OPTION_CHANNEL": "Radio channel or genre to play (e.g. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Usage: `{{prefix}}radio [playlist name] [channel name]` (e.g. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Please provide both playlist name and channel name", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Could not find a radio channel matching `{{channelQuery}}` in playlist `{{playlistName}}`.\nAvailable examples: {{samples}} etc.", - "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\n(e.g., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radio playlist is not ready.\nPlease import radio playlist first with `/playlist import {{name}} `.\nThe file must be an M3U playlist with EXTINF tags.", "ERROR_RADIO_NO_MATCH": "❌ Radio channel matching `{{query}}` not found.\nAvailable examples: {{samples}} etc.", "ERROR_RADIO_LOAD_FAILED": "❌ Error occurred during loading radio channel: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Searching radio channel **{{title}}** in playlist **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Could not load or find radio stream (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Searching radio channel **{{title}}** in playlist **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Started playing radio channel **{{title}}** from playlist **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Page {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Only the command user can navigate pages.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Only the command user can confirm this action.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Only the command user can navigate pages", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Only the command user can confirm this action", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Playlist Exists", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "A playlist named **`{{name}}`** ({{count}} tracks) already exists.\nDo you want to overwrite it with the imported playlist?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "A playlist named **`{{name}}`** ({{count}} tracks) already exists.\nDo you want to overwrite it with the current queue?", "LABEL_OVERWRITE": "Overwrite", "LABEL_CANCEL": "Cancel", - "MESSAGE_PLAYLIST_OVERWRITING": "Overwriting playlist...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Playlist import cancelled.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty." -} \ No newline at end of file + "MESSAGE_PLAYLIST_OVERWRITING": "Overwriting playlist..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Playlist import cancelled", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Import confirmation timed out", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Playlist save cancelled", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Save confirmation timed out", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Skipped current track. Queue is now empty.", + "ERROR_PLAYLIST_DISABLED": "❌ Playlist feature is disabled.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U playlists cannot be queued entirely. Please use the `/radio` command to select a specific station.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U playlist import is restricted to Admins and DJs only." +} diff --git a/src/locales/en-US/embeds.json b/src/locales/en-US/embeds.json index 6dc4ff3d..0027ed9f 100644 --- a/src/locales/en-US/embeds.json +++ b/src/locales/en-US/embeds.json @@ -4,16 +4,16 @@ "DASHBOARD_SUBTITLE": "Author: **{{author}}**\nDuration: **{{duration}}**\n────────────────────\nVolume: `{{volume}}%`\nLoop: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Requester:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Dynamic DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Voice channel connected successfully.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Connected to the voice channel", "MAINTAIN_DESCRIPTION": "The bot will begin maintenance in a few minutes. During this time, some features may be temporarily unavailable.", "MAINTAIN_TITLE": "⚠️ Maintenance Notice ⚠️", - "MESSAGE_BLACKLIST": "There are blacklisted user(s) in the voice channel.", + "MESSAGE_BLACKLIST": "There are blacklisted users in the voice channel", "MESSAGE_COMMAND": "Command **{{command}}**", - "MESSAGE_FILTER": "Set filter mode to **{{effectName}}**", + "MESSAGE_FILTER": "Filter set to: **{{effectName}}**", "NODE_ALL_ACTIVE": "All nodes are active", "NODE_CONNECTED": "Latency: **{{ping}}ms**", "NODE_DISCONNECTED": "**DISCONNECTED**", - "NODE_STATUS_ARGS_ERROR": "❌ Please enter a valid node name.", + "NODE_STATUS_ARGS_ERROR": "❌ Please enter a valid node name", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Cores: **{{cores}}**\nSystem load: **{{systemLoad}}**\nLavalink load: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ INFO", @@ -22,14 +22,14 @@ "NODE_STATUS_MEMORY_VALUE": "Used: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", "NODE_STATUS_PING": "Latency", "NODE_STATUS_STATS_TITLE": "📊 STATS", - "NODE_STATUS_STATS_VALUE": "uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "Uptime: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ Nodes Status", "NODE_STATUS_TITLE_2": "🛰️ Node '{{nodeName}}' status", "NODE_UNHEALTHY": "⚠️ There are {{unhealthValue}} nodes disconnected", "PING_DESCRIPTION": "Bot: **{{botPing}}**\nAPI: **{{apiPing}}ms**", - "PING_TITLE": "🛰️ LATENCY'", - "QUEUE_ADD_PLAYLIST": "Added Playlist", - "QUEUE_ADD_TRACK": "Added Track", + "PING_TITLE": "🛰️ LATENCY", + "QUEUE_ADD_PLAYLIST": "Playlist added", + "QUEUE_ADD_TRACK": "Track added", "QUEUE_LIST_LOOP_MODE": "Loop: {{repeatMode}}", "QUEUE_LIST_TITLE": "Queue List", "REMOVE_LIST_LOOP_MODE": "Loop: {{repeatMode}}", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Now Playing:**", "QUEUE_EMPTY": "*No songs in queue*", "QUEUE_HEADER": "**Queue:**" -} \ No newline at end of file +} diff --git a/src/locales/en-US/events.json b/src/locales/en-US/events.json index 4db7d42a..89ab1c11 100644 --- a/src/locales/en-US/events.json +++ b/src/locales/en-US/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Unable to get guild data in cache.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ You are not on the same audio channel as me.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Please join the specified voice channel <#{{channelId}}> and call me.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ You are not connected to an audio channel.", - "ERROR_NOT_PLAYING": "❌ There is no music currently playing.", - "ERROR_REQUIRE_ADMIN": "❌ This command requires administrator privileges.", - "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message.", - "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled.", - "ERROR_TIME_EXPIRED": "❌ Time expired.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left.", - "MESSAGE_BOT_STOP": "✅ Bot stopped.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Unable to get guild data in cache", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ You are not on the same audio channel as me", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Please join the specified voice channel <#{{channelId}}> and call me", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ You are not connected to an audio channel", + "ERROR_NOT_PLAYING": "❌ There is no music currently playing", + "ERROR_REQUIRE_ADMIN": "❌ This command requires administrator privileges", + "ERROR_REQUIRE_DJ": "❌ This command requires DJ privileges", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ I can't send you a private message", + "ERROR_SLASH_NOT_ENABLE": "❌ The slash command is not enabled", + "ERROR_TIME_EXPIRED": "❌ The wait time has expired", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot left", + "MESSAGE_BOT_STOP": "✅ Bot stopped", + "MESSAGE_MUSIC_SHUFFLE": "✅ Music shuffled", "MESSAGE_NOW_PLAYING_SUBTITLE": "Author: **{{author}}**\nDuration: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Now Playing: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Page {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Select song loop mode.", + "MESSAGE_SELECT_LOOP_MODE": "Select song loop mode", "LOOP_MODE_OFF": "Off", "LOOP_MODE_SINGLE": "Single", "LOOP_MODE_ALL": "All", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I sent you the name of the music in a private message.", - "MESSAGE_SET_LOOP_MODE": "✅ Set loop to `{{mode}}`", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ I've sent you the track name in a private message", + "MESSAGE_SET_LOOP_MODE": "✅ Loop mode set to `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ You can not execute commands in this channel. Please use the <#{{channelId}}> text channel." -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/commands.json b/src/locales/ko-KR/commands.json index d7fc1eae..7e891761 100644 --- a/src/locales/ko-KR/commands.json +++ b/src/locales/ko-KR/commands.json @@ -6,138 +6,138 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "사용자를 블랙리스트에서 제거합니다", "CONFIG_BLACKLIST_OPTION_LIST": "블랙리스트 사용자 목록을 조회합니다", "CONFIG_BLACKLIST_OPTION_USER": "블랙리스트에 추가하거나 제거할 사용자", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 동적 블랙리스트에 없습니다.", - "MESSAGE_BLACKLIST_LIST_TITLE": "동적 블랙리스트", - "MESSAGE_BLACKLIST_LIST_EMPTY": "동적 블랙리스트가 비어 있습니다.", - "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}>이(가) 블랙리스트에 추가되었습니다", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}>이(가) 블랙리스트에서 제거되었습니다", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}>은(는) 이미 블랙리스트에 있습니다", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}>은(는) 블랙리스트에 없습니다", + "MESSAGE_BLACKLIST_LIST_TITLE": "블랙리스트", + "MESSAGE_BLACKLIST_LIST_EMPTY": "블랙리스트가 현재 비어 있습니다", + "CONFIG_CLEAR_DESCRIPTION": "재생목록을 초기화합니다", "CONFIG_CLEAR_USAGE": "clear", - "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다.", + "CONFIG_DASHBOARD_DESCRIPTION": "대시보드를 최하단으로 이동합니다", "CONFIG_DASHBOARD_USAGE": "dashboard", - "CONFIG_DJ_DESCRIPTION": "사용자를 DJ로 추가하거나 DJ 목록을 표시합니다.", + "CONFIG_DJ_DESCRIPTION": "사용자를 DJ로 추가하거나 DJ 목록을 표시합니다", "CONFIG_DJ_USAGE": "dj [@사용자]", "CONFIG_DJ_OPTION_DESCRIPTION": "DJ로 추가할 사용자 (선택사항)", - "CONFIG_FILTER_DESCRIPTION": "재생 효과를 변경합니다.", + "CONFIG_FILTER_DESCRIPTION": "재생 효과를 변경합니다", "CONFIG_FILTER_USAGE": "filter [효과명]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "원하는 음악 효과 모드를 입력 및 선택하세요.", - "CONFIG_HELP_DESCRIPTION": "명령어 도움말을 확인합니다.", + "CONFIG_FILTER_OPTION_DESCRIPTION": "원하는 음악 효과 모드를 입력 및 선택하세요", + "CONFIG_HELP_DESCRIPTION": "명령어 도움말을 확인합니다", "CONFIG_HELP_USAGE": "help [명령어]", - "CONFIG_HELP_OPTION_DESCRIPTION": "도움이 필요한 명령어를 입력하세요.", - "CONFIG_JOIN_DESCRIPTION": "현재 음성 채널에 봇을 참여시킵니다.", + "CONFIG_HELP_OPTION_DESCRIPTION": "도움이 필요한 명령어를 입력하세요", + "CONFIG_JOIN_DESCRIPTION": "현재 음성 채널에 봇을 참여시킵니다", "CONFIG_JOIN_USAGE": "join", - "CONFIG_LANG_DESCRIPTION": "봇 언어를 변경합니다.", + "CONFIG_LANG_DESCRIPTION": "봇 언어를 변경합니다", "CONFIG_LANG_USAGE": "language [언어]", "CONFIG_LANG_OPTION_DESCRIPTION": "언어", - "CONFIG_LEAVE_DESCRIPTION": "현재 음성 채널에서 봇을 내보냅니다.", + "CONFIG_LEAVE_DESCRIPTION": "현재 음성 채널에서 봇을 내보냅니다", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "반복 모드를 켜거나 끕니다.", + "CONFIG_LOOP_DESCRIPTION": "반복 모드를 켜거나 끕니다", "CONFIG_LOOP_USAGE": "loop [끄기/한 곡 반복/전체 반복]", - "CONFIG_LOOP_OPTION_DESCRIPTION": "반복 모드를 선택하세요.", - "CONFIG_MOVE_DESCRIPTION": "재생목록에 있는 두 곡의 위치를 바꿉니다.", + "CONFIG_LOOP_OPTION_DESCRIPTION": "반복 모드를 선택하세요", + "CONFIG_MOVE_DESCRIPTION": "재생목록에 있는 두 곡의 위치를 바꿉니다", "CONFIG_MOVE_USAGE": "move [번호 1] [번호 2]", "CONFIG_MOVE_OPTION_DESCRIPTION": "바뀔 재생목록 번호", "CONFIG_MOVE_OPTION_DESCRIPTION_2": "바꾸고자 하는 재생목록 번호", - "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다.", + "CONFIG_NODE_DESCRIPTION": "노드 상태를 표시합니다", "CONFIG_NODE_USAGE": "node [노드명]", - "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요.", - "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악를 표시합니다.", + "CONFIG_NODE_OPTION_DESCRIPTION": "노드 이름을 입력하세요", + "CONFIG_NOW_PLAYING_DESCRIPTION": "지금 재생 중인 음악을 표시합니다", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", - "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다.", + "CONFIG_PAUSE_DESCRIPTION": "음악을 임시 중지합니다", "CONFIG_PAUSE_USAGE": "pause", - "CONFIG_PING_DESCRIPTION": "서버와 봇의 지연시간을 확인합니다.", + "CONFIG_PING_DESCRIPTION": "서버와 봇의 지연시간을 확인합니다", "CONFIG_PING_USAGE": "ping", - "CONFIG_PLAY_DESCRIPTION": "재생할 음악 링크 또는 음악 이름을 입력하세요.", + "CONFIG_PLAY_DESCRIPTION": "재생할 음악 링크 또는 음악 이름을 입력하세요", "CONFIG_PLAY_USAGE": "play [음악 링크/음악 이름]", - "CONFIG_PLAY_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요.", - "CONFIG_PLAYFIRST_DESCRIPTION": "먼저 재생할 음악 링크 혹은 음악 이름을 입력하세요.", + "CONFIG_PLAY_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요", + "CONFIG_PLAYFIRST_DESCRIPTION": "먼저 재생할 음악 링크 혹은 음악 이름을 입력하세요", "CONFIG_PLAYFIRST_USAGE": "playfirst [음악 링크/음악 이름]", - "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요.", + "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "음악 링크 혹은 음악 이름을 입력하세요", "CONFIG_PLAYLAST_DESCRIPTION": "현재 또는 마지막으로 재생된 곡을 다시 재생합니다", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ 이 명령은 대기열이 비어있을 때만 사용할 수 있습니다.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ 최근 재생된 곡을 찾을 수 없습니다.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ 곡이 대기열에 다시 추가되었습니다.", - "CONFIG_QUEUE_DESCRIPTION": "현재 재생중인 음악을 확인합니다.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ 이 명령은 대기열이 비어있을 때만 사용할 수 있습니다", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ 최근 재생된 곡을 찾을 수 없습니다", + "MESSAGE_PLAYLAST_SUCCESS": "✅ 곡이 대기열에 다시 추가되었습니다", + "CONFIG_QUEUE_DESCRIPTION": "현재 재생중인 음악을 확인합니다", "CONFIG_QUEUE_USAGE": "queue", - "CONFIG_REMOVE_DESCRIPTION": "재생목록에서 제거할 음악을 선택합니다.", + "CONFIG_REMOVE_DESCRIPTION": "재생목록에서 제거할 음악을 선택합니다", "CONFIG_REMOVE_USAGE": "remove [재생목록 번호]", "CONFIG_REMOVE_OPTION_DESCRIPTION": "재생목록 번호", "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "재생목록 번호", - "CONFIG_RESUME_DESCRIPTION": "일시중지된 음악을 재생합니다.", + "CONFIG_RESUME_DESCRIPTION": "일시중지된 음악을 재생합니다", "CONFIG_RESUME_USAGE": "resume", - "CONFIG_SEARCH_DESCRIPTION": "검색할 음악 이름 혹은 링크를 입력하세요.", + "CONFIG_SEARCH_DESCRIPTION": "검색할 음악 이름 혹은 링크를 입력하세요", "CONFIG_SEARCH_USAGE": "search [음악 이름/음악 링크]", - "CONFIG_SEARCH_OPTION_DESCRIPTION": "음악 이름 혹은 음악 링크를 입력하세요.", - "CONFIG_SEEK_DESCRIPTION": "음악을 특정 시간으로 이동합니다.", + "CONFIG_SEARCH_OPTION_DESCRIPTION": "음악 이름 혹은 음악 링크를 입력하세요", + "CONFIG_SEEK_DESCRIPTION": "음악을 특정 시간으로 이동합니다", "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (예시: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요.", - "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다.", + "CONFIG_SEEK_OPTION_DESCRIPTION": "이동할 시간 (예시: 3m20s, 1:20:55)을 입력하세요", + "CONFIG_SERVER_DESCRIPTION": "봇이 가입된 서버를 표시합니다", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "음악을 랜덤으로 재생할지 선택합니다.", + "CONFIG_SHUFFLE_DESCRIPTION": "재생목록을 섞습니다", "CONFIG_SHUFFLE_USAGE": "shuffle", - "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다.", + "CONFIG_SKIP_DESCRIPTION": "재생 중인 음악을 건너뜁니다", "CONFIG_SKIP_USAGE": "skip", - "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다.", + "CONFIG_STATUS_DESCRIPTION": "봇의 상태를 표시합니다", "CONFIG_STATUS_USAGE": "status", - "CONFIG_STOP_DESCRIPTION": "현재 재생 중인 음악을 멈추고 봇을 내보냅니다.", + "CONFIG_STOP_DESCRIPTION": "재생을 멈추고 재생목록을 비웁니다. 채널은 떠나지 않습니다.", "CONFIG_STOP_USAGE": "stop", - "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다.", + "CONFIG_VOLUME_DESCRIPTION": "재생 음량을 설정합니다", "CONFIG_VOLUME_USAGE": "v [0-200]", - "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정하고자 하는 음량 값을 입력하세요. (1~{{maxVolume}})", - "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다.", + "CONFIG_VOLUME_OPTION_DESCRIPTION": "설정할 음량", + "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "이 서버의 기본 음량을 변경합니다", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [음량]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "설정할 기본 음량 (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ 명령에 오류가 발생했습니다. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ 현재 대기열에 재생할 음악이 없습니다.", - "ERROR_NO_PLAYING": "❌ 현재 재생 중인 음악이 없습니다.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ 현재 대기열에 재생할 음악이 없습니다", + "ERROR_NO_PLAYING": "❌ 현재 재생 중인 음악이 없습니다", "ERROR_SKIP_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 건너뛸 수 있습니다. 관리자 및 DJ(활성화된 경우)도 건너뛸 수 있습니다.", "ERROR_SEEK_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 탐색할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 탐색할 수 있습니다.", "ERROR_PAUSE_NOT_REQUESTER": "❌ 이 곡을 요청한 사람만 일시정지할 수 있습니다. 관리자 및 DJ(활성화된 경우)도 일시정지할 수 있습니다.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다.", - "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 다시 시도해 주세요. 이유: ({{reason}})", + "ERROR_PLAY_JOIN_CHANNEL": "❌ 음성 채널에 참여할 수 없습니다", + "ERROR_PLAY_MUSIC": "❌ 서비스에 문제가 발생했습니다. 잠시 후 다시 시도해 주세요. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ 검색결과가 없습니다. 이유: ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", - "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다.", - "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다.", - "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다.", - "MESSAGE_DJ_MENTION_USER": "DJ로 추가할 사용자를 멘션해주세요.", - "MESSAGE_DJ_NO_BOTS": "봇을 DJ로 추가할 수 없습니다.", - "MESSAGE_DJ_STATIC_MODE": "고정 모드에서는 설정을 통해 DJ 권한이 관리됩니다.", - "MESSAGE_DJ_NO_PLAYER": "활성화된 음악 플레이어를 찾을 수 없습니다.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}>는 이미 DJ입니다.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}>를 DJ로 성공적으로 추가했습니다.", + "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다", + "MESSAGE_CLEAR_SUCCESS": "✅ 재생목록이 초기화되었습니다", + "MESSAGE_DASHBOARD_SUCCESS": "✅ 대시보드가 업데이트되었습니다", + "MESSAGE_DJ_NO_PERMISSION": "DJ를 추가하려면 DJ 또는 관리자 권한이 필요합니다", + "MESSAGE_DJ_ADMIN_ONLY": "관리자만 DJ를 추가하거나 제거할 수 있습니다", + "MESSAGE_DJ_MENTION_USER": "DJ로 추가할 사용자를 멘션해주세요", + "MESSAGE_DJ_NO_BOTS": "봇을 DJ로 추가할 수 없습니다", + "MESSAGE_DJ_STATIC_MODE": "고정 모드에서는 설정을 통해 DJ 권한이 관리됩니다", + "MESSAGE_DJ_NO_PLAYER": "활성화된 음악 플레이어를 찾을 수 없습니다", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}>는 이미 DJ입니다", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}>를 DJ로 성공적으로 추가했습니다", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ가 채널을 떠났습니다! 새로운 DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ 목록", "MESSAGE_DJ_LIST_ADMINS": "관리자", "MESSAGE_DJ_LIST_ROLE_DJS": "역할 기반 DJ", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "동적 DJ", - "MESSAGE_DJ_LIST_NONE": "구성된 DJ가 없습니다.", + "MESSAGE_DJ_LIST_NONE": "구성된 DJ가 없습니다", "MESSAGE_DJ_LIST_ERROR": "❌ DJ 목록을 불러오지 못했습니다. 다시 시도해 주세요.", "MESSAGE_DJ_ROLE_NOT_SET": "DJ 역할이 구성되지 않음", - "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다.", - "MESSAGE_FILTER_SELECT_LIST": "⬇️ 설정할 재생 효과를 선택합니다. ⬇️", + "MESSAGE_FILTER_NOT_FOUND": "❌ 효과명을 찾을 수 없습니다", + "MESSAGE_FILTER_SELECT_LIST": "설정할 재생 효과를 선택하세요: ⬇️", "MESSAGE_FILTER_SELECT_MODE": "필터 모드 선택", - "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다.", - "MESSAGE_HELP_SELECT_LIST": "⬇️ 도움말을 받으려면 명령어 선택하세요. ⬇️", + "MESSAGE_HELP_NOT_FOUND": "❌ 명령어를 찾을 수 없습니다", + "MESSAGE_HELP_SELECT_LIST": "도움말을 받으려면 명령어를 선택하세요. ⬇️", "MESSAGE_HELP_SELECT_MODE": "도움말 선택하기", - "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다.", + "MESSAGE_JOIN_SUCCESS": "✅ 봇이 입장했습니다", "MESSAGE_LANG_ARGS_ERROR": "언어 변수가 잘못되었습니다. (현재 사용 가능한 언어: {{langList}})", "MESSAGE_LANG_AVAILABLE_LIST": "현재 사용 가능한 언어: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ 봇의 언어가 `{{locale}}`(으)로 성공적으로 변경되었습니다.", - "MESSAGE_LEAVE_SUCCESS": "✅ 봇이 떠났습니다.", + "MESSAGE_LANG_SUCCESS": "✅ 봇의 언어가 `{{locale}}`(으)로 성공적으로 변경되었습니다", + "MESSAGE_LEAVE_SUCCESS": "✅ 봇이 떠났습니다", "REPEAT_MODE_OFF": "끄기", "REPEAT_MODE_SINGLE": "한 곡 반복", "REPEAT_MODE_ALL": "전체 반복", "LOOP_CHOICE_OFF": "끄기", "LOOP_CHOICE_ONE": "한 곡 반복", "LOOP_CHOICE_ALL": "전체 반복", - "LOOP_SELECT_PLACEHOLDER": "반복 모드를 선택하세요.", - "LOOP_SELECT_DESCRIPTION": "반복 모드를 {{mode}}(으)로 설정합니다.", - "MESSAGE_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다.", - "MESSAGE_MOVE_SUCCESS": "✅ 음악 순서가 성공적으로 변경되었습니다.", + "LOOP_SELECT_PLACEHOLDER": "반복 모드를 선택하세요", + "LOOP_SELECT_DESCRIPTION": "반복 모드를 {{mode}}(으)로 설정합니다", + "MESSAGE_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다", + "MESSAGE_MOVE_SUCCESS": "✅ 음악 순서가 성공적으로 변경되었습니다", "MESSAGE_MOVE_WRONG_INDEX": "❌ 유효한 범위를 입력하세요. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\n그 외 {{length}}개의 곡", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "음악 저장", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 현재 재생 중: {{title}} | {{requester}} 요청", "UNKNOWN_USER": "알 수 없음", - "MESSAGE_PAUSE_FAIL": "❌ 음악 일시 중지에 실패했습니다.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ 음악이 일시정지되었습니다.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ 음악이 일시정지되었습니다.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ 검색하려는 음악의 이름을 입력하세요.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ 음악이 추가되었습니다.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ 검색결과가 없습니다.", + "MESSAGE_PAUSE_FAIL": "❌ 음악 일시 중지에 실패했습니다", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ 음악이 일시정지되었습니다", + "MESSAGE_PAUSE_SUCCESS": "⏸️ 음악이 일시정지되었습니다", + "MESSAGE_PLAY_ARGS_ERROR": "❌ 검색하려는 음악의 이름을 입력하세요", + "MESSAGE_PLAY_MUSIC_ADD": "✅ 음악이 추가되었습니다", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ 검색결과가 없습니다", "MESSAGE_PLAY_SELECT_DURATION": "**길이:** {{label}}", - "MESSAGE_PLAY_SELECT_TITLE": "음악을 선택하세요.", - "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다.", - "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다.", + "MESSAGE_PLAY_SELECT_TITLE": "음악을 선택하세요", + "MESSAGE_REMOVE_CANCEL": "✅ 음악 제거를 취소했습니다", + "MESSAGE_REMOVE_FAIL": "❌ 음악 제거에 실패했습니다", "MESSAGE_REMOVE_INSTRUCTION": "⬇️ **1 ~ {{length}}** 중에서 노래를 선택하여 **제거**하거나 다른 문자를 입력하여 선택을 취소합니다. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 대기열에 음악이 없습니다.", - "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ 음악이 다시 재생되었습니다.", - "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다.", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 현재 곡 이후 대기열이 비어 있습니다", + "MESSAGE_RESUME_FAIL": "❌ 음악이 다시 재생에 실패했습니다", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 음악이 다시 재생되었습니다", + "MESSAGE_RESUME_SUCCESS": "▶️ 음악이 다시 재생되었습니다", "MESSAGE_SEEK_ARGS_ERROR": "❌ 목표 시간이 잘못된 형식입니다. (예: 3m20s, 1m 50s, 1:20:55, 5:20)", "MESSAGE_SEEK_SKIP": "✅ 해당 위치는 {{duration}}을 초과합니다.\n다음 곡으로 건너뜁니다.", - "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 음악을 랜덤으로 재생합니다.", - "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다.", - "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다.", - "MESSAGE_STOP_SUCCESS": "✅ 음악을 멈췄습니다.", - "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다.", - "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\\n**설정할 음량을 선택하세요:**", - "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다.", - "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}%**로 변경되었습니다!", + "MESSAGE_SEEK_SUCCESS": "✅ 음악이 `{{duration}}`로 이동되었습니다", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 재생목록을 섞었습니다", + "MESSAGE_SKIP_FAIL": "❌ 음악 건너뛰기에 실패했습니다", + "MESSAGE_SKIP_SUCCESS": "✅ 음악을 건너뜁니다", + "MESSAGE_STOP_SUCCESS": "✅ 재생을 멈추고 재생목록을 비웠습니다", + "MESSAGE_VOLUME_ARGS_ERROR": "**현재 음량:** 🔊 {{volume}}%\n음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력합니다", + "MESSAGE_VOLUME_SELECT": "**현재 음량:** 🔊 {{volume}}%\n**설정할 음량을 선택하세요:**", + "MESSAGE_VOLUME_SAME": "❌ 변경하려는 음량은 이미 현재 볼륨입니다", + "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 이 서버의 기본 음량이 **{{volume}}%**로 변경되었습니다! (최대: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "현재 기본 음량: **{{volume}}%** 🔊\n기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 기본 음량을 변경하려면 `1`에서 `{{maxVolume}}` 사이의 숫자를 입력하세요", "ERROR_QUEUE_LIMIT_REACHED": "❌ 대기열 한도에 도달했습니다! 현재 대기열에 **{{current}}**개의 음악이 있습니다 (한도: **{{limit}}**). 더 추가하려면 일부 음악이 끝날 때까지 기다려 주세요.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ 전역 대기열 한도에 도달했습니다! 대기열에 현재 **{{total}}**개의 음악이 있습니다 (전역 한도: **{{limit}}**). 더 추가하려면 일부 음악이 끝날 때까지 기다려 주세요.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (대기열 한도: **{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (전역 대기열 한도: **{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (대기열 한도: **{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 플레이리스트 일부 추가됨: **{{added}}**개 추가, **{{skipped}}**개 건너뜀 (전역 대기열 한도: **{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "서버 ID", "MESSAGE_SERVER_GUILD": "서버", "MESSAGE_SERVER_MEMBERS": "멤버", @@ -186,7 +186,7 @@ "MESSAGE_DJ_TYPE_STATIC": "(고정)", "MESSAGE_DJ_TYPE_ROLE": "(역할)", "MESSAGE_DJ_TYPE_DYNAMIC": "(동적)", - "LABEL_DELETE_MESSAGE": "메세지 삭제", + "LABEL_DELETE_MESSAGE": "메시지 삭제", "LABEL_CLEAR_QUEUE": "재생목록 초기화", "UNIT_DAYS": "일", "UNIT_HOURS": "시간", @@ -197,73 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "블랙리스트 관리자가 초기화되지 않았습니다.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "블랙리스트 관리자가 초기화되지 않았습니다", "HELP_SELECT_MUSIC_PLACEHOLDER": "음악 명령어 선택", - "HELP_SELECT_UTILITY_PLACEHOLDER": "유틸리티 명령어 선택", + "HELP_SELECT_UTILITY_PLACEHOLDER": "서비스 명령어를 선택하세요", "HELP_COMMAND_ALIASES": "별칭: {{aliases}}", "HELP_COMMAND_NONE": "없음", "LABEL_FILTER_CLEAR": "효과 초기화", - "CONFIG_PLAYLIST_DESCRIPTION": "플레이리스트를 저장, 로드, 편집 또는 외부 M3U에서 가져옵니다.", + "CONFIG_PLAYLIST_DESCRIPTION": "플레이리스트를 저장, 로드, 편집 또는 외부 M3U에서 가져옵니다", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "현재 대기열을 플레이리스트로 저장합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "저장된 플레이리스트를 대기열에 추가하고 재생합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "서버의 플레이리스트 목록을 조회합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "플레이리스트에 저장된 곡 목록을 확인합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "플레이리스트를 삭제합니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "외부 M3U 파일 주소에서 플레이리스트를 가져옵니다.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "플레이리스트에서 특정 트랙을 삭제합니다.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "현재 대기열을 플레이리스트로 저장합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "저장된 플레이리스트를 대기열에 추가하고 재생합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "서버의 플레이리스트 목록을 조회합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "플레이리스트에 저장된 곡 목록을 확인합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "플레이리스트를 삭제합니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "외부 M3U 파일 주소에서 플레이리스트를 가져옵니다", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "플레이리스트에서 특정 트랙을 삭제합니다", "CONFIG_PLAYLIST_OPTION_NAME": "플레이리스트 이름", "CONFIG_PLAYLIST_OPTION_SEARCH": "플레이리스트 내부에서 검색하여 재생할 트랙 이름 (옵션)", "CONFIG_PLAYLIST_OPTION_URL": "M3U 파일 다운로드 링크", "CONFIG_PLAYLIST_OPTION_INDEX": "삭제할 트랙 번호 (1부터 시작)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ 플레이리스트 매니저가 초기화되지 않았습니다.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ 올바른 하위 명령어를 입력해주세요 (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ 알 수 없는 하위 명령어입니다.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ 현재 재생 중이거나 대기열에 곡이 없습니다.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ 플레이리스트 이름을 입력해주세요.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ 플레이리스트 저장에 실패했습니다.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ 플레이리스트 `{{name}}`을(를) 찾을 수 없거나 비어 있습니다.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ 플레이리스트 `{{name}}` 내에서 `{{searchQuery}}`와(과) 일치하는 트랙을 찾을 수 없습니다.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ 플레이리스트 매니저가 초기화되지 않았습니다", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ 올바른 하위 명령어를 입력해주세요 (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ 알 수 없는 하위 명령어입니다", + "ERROR_PLAYLIST_NO_PLAYING": "❌ 현재 재생 중이거나 대기열에 곡이 없습니다", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ 플레이리스트 이름을 입력해주세요", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ 플레이리스트 저장에 실패했습니다", + "ERROR_PLAYLIST_NOT_FOUND": "❌ 플레이리스트 `{{name}}`을(를) 찾을 수 없거나 비어 있습니다", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ 플레이리스트 `{{name}}` 내에서 `{{searchQuery}}`와(과) 일치하는 트랙을 찾을 수 없습니다", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ M3U 파일을 다운로드하지 못했습니다. (HTTP 상태 코드: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ 보안 정책에 의해 M3U 다운로드가 차단되었습니다. {{timeoutSeconds}}초 이내에 응답하고 {{maxSizeMiB}} MiB 이하인 공개 HTTPS URL을 사용해주세요.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ 플레이리스트에는 최대 {{limit}}개의 트랙만 저장할 수 있습니다.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ 플레이리스트에는 최대 {{limit}}개의 트랙만 저장할 수 있습니다", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ M3U 플레이리스트 파싱 및 저장에 실패했습니다. 올바른 형식인지 확인해주세요.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 곡 삭제에 실패했습니다. 올바른 트랙 번호인지 확인해주세요.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드가 완료되었습니다. (성공: {{added}}곡, 실패/스킵: {{skipped}}곡)", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 현재 대기열의 {{count}}곡을 플레이리스트 `{{name}}`로 저장했습니다", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 플레이리스트 `{{name}}`에서 {{count}}개의 곡 검색 및 대기열 추가를 시작합니다..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 플레이리스트 `{{name}}` 로드 완료: {{added}}곡 추가, {{skipped}}곡 건너뜀", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 저장된 플레이리스트가 없습니다", "MESSAGE_PLAYLIST_LIST_TITLE": "서버 플레이리스트 목록", "MESSAGE_PLAYLIST_INFO_TITLE": "플레이리스트 정보: {{name}} (총 {{count}}곡)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*외 {{remaining}}개의 곡이 더 있습니다.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ 플레이리스트 `{{name}}`을(를) 삭제했습니다.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ 플레이리스트 `{{name}}`을(를) 삭제했습니다", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ 외부 M3U 플레이리스트를 성공적으로 가져왔습니다!\n`{{name}}` 플레이리스트에 {{count}}개의 곡이 등록되었습니다.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ 플레이리스트 `{{name}}`에서 {{index}}번 곡을 삭제했습니다.", - "CONFIG_RADIO_DESCRIPTION": "가져온 라디오 플레이리스트에서 특정 장르/채널을 검색해 바로 재생합니다.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ 플레이리스트 `{{name}}`에서 {{index}}번 곡을 삭제했습니다", + "CONFIG_RADIO_DESCRIPTION": "가져온 라디오 플레이리스트에서 특정 장르/채널을 검색해 바로 재생합니다", "CONFIG_RADIO_USAGE": "radio [채널명]", "CONFIG_RADIO_OPTION_PLAYLIST": "검색할 플레이리스트 이름", "CONFIG_RADIO_OPTION_CHANNEL": "재생할 라디오 채널명 또는 장르 (예: workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ 올바른 사용법: `{{prefix}}radio [플레이리스트 이름] [채널명]` (예: `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ 플레이리스트 이름과 검색할 채널명을 모두 입력해주세요", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 플레이리스트 `{{playlistName}}`에서 `{{channelQuery}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", - "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n(예: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ 라디오 플레이리스트가 준비되지 않았습니다.\n먼저 `/playlist import {{name}} ` 명령어로 라디오 주소 목록을 임포트해 주세요.\n파일은 EXTINF 태그가 포함된 M3U 형식이어야 합니다.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}`와(과) 일치하는 라디오 채널을 찾을 수 없습니다.\n사용 가능한 예시: {{samples}} 등", "ERROR_RADIO_LOAD_FAILED": "❌ 라디오 채널 로딩 중 오류가 발생했습니다: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다.", - "MESSAGE_RADIO_SEARCHING": "🔄 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 검색 중...", + "ERROR_RADIO_STREAM_FAILED": "❌ 라디오 스트림(`{{title}}`)을 찾거나 로드할 수 없습니다", + "MESSAGE_RADIO_SEARCHING": "🔄 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 검색 중..", "MESSAGE_RADIO_PLAYING": "📻 플레이리스트 **{{playlist}}**의 라디오 채널 **{{title}}** 재생을 시작합니다!", "MESSAGE_FOOTER_PAGE": "페이지 {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "명령어를 실행한 사용자만 페이지를 이동할 수 있습니다.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "명령어를 실행한 사용자만 이 작업을 승인할 수 있습니다.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "명령어를 실행한 사용자만 페이지를 이동할 수 있습니다", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "명령어를 실행한 사용자만 이 작업을 승인할 수 있습니다", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ 플레이리스트가 이미 존재합니다", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "**`{{name}}`** 이름의 플레이리스트(총 {{count}}곡)가 이미 존재합니다.\n가져온 플레이리스트로 덮어쓰시겠습니까?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "**`{{name}}`** 이름의 플레이리스트(총 {{count}}곡)가 이미 존재합니다.\n현재 대기열의 트랙으로 덮어쓰시겠습니까?", "LABEL_OVERWRITE": "덮어쓰기", "LABEL_CANCEL": "취소", - "MESSAGE_PLAYLIST_OVERWRITING": "플레이리스트 덮어쓰는 중...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 플레이리스트 가져오기가 취소되었습니다.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다." -} \ No newline at end of file + "MESSAGE_PLAYLIST_OVERWRITING": "플레이리스트 덮어쓰는 중..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 플레이리스트 가져오기가 취소되었습니다", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 가져오기 확인 시간이 초과되었습니다", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 플레이리스트 저장이 취소되었습니다", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 저장 확인 시간이 초과되었습니다", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 현재 곡을 건너뛰었습니다. 대기열이 비어있습니다.", + "ERROR_PLAYLIST_DISABLED": "❌ 플레이리스트 기능이 비활성화되어 있습니다.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U 플레이리스트 전체는 재생할 수 없습니다. `/radio` 명령어를 사용하여 원하는 라디오 채널을 선택해 주세요.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U 플레이리스트 가져오기는 관리자 또는 DJ 권한이 있는 사용자만 수행할 수 있습니다." +} diff --git a/src/locales/ko-KR/embeds.json b/src/locales/ko-KR/embeds.json index 3929475b..ab2cb737 100644 --- a/src/locales/ko-KR/embeds.json +++ b/src/locales/ko-KR/embeds.json @@ -1,40 +1,40 @@ { "DASHBOARD_TITLE": "대시보드", - "DASHBOARD_FINISH_PLAYING": "모든 음악이 재생되었습니다.", + "DASHBOARD_FINISH_PLAYING": "재생목록 재생이 완료되었습니다", "DASHBOARD_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{duration}}\n────────────────────\n**음량:** `{{volume}}%`\n**반복:** `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**요청자:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성채팅에 성공적으로 연결했습니다.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "음성 채널에 성공적으로 연결했습니다", "MAINTAIN_DESCRIPTION": "몇 분 후에 봇이 점검을 시작합니다. 일부 기능을 일시적으로 사용하지 못할 수 있습니다.", "MAINTAIN_TITLE": "⚠️ 유지보수 안내 ⚠️", - "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다.", + "MESSAGE_BLACKLIST": "음성 채널에 블랙리스트에 등록된 사용자가 있습니다", "MESSAGE_COMMAND": "명령어 **{{command}}**", - "MESSAGE_FILTER": "`{{effectName}}`(으)로 재생 효과가 변경되었습니다.", - "NODE_ALL_ACTIVE": "모든 노드가 활성화되었습니다.", + "MESSAGE_FILTER": "`{{effectName}}`(으)로 재생 효과가 변경되었습니다", + "NODE_ALL_ACTIVE": "모든 노드가 활성화되었습니다", "NODE_CONNECTED": "지연시간: **{{ping}}ms**", "NODE_DISCONNECTED": "**연결 끊김**", - "NODE_STATUS_ARGS_ERROR": "❌ 유효한 노드 이름을 입력하세요.", + "NODE_STATUS_ARGS_ERROR": "❌ 유효한 노드 이름을 입력하세요", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "**코어:** {{cores}}\n**시스템 로드:** {{systemLoad}}\n**Lavalink 로드:** {{lavalinkLoad}}", "NODE_STATUS_INFO_TITLE": "🏷️ 정보", "NODE_STATUS_INFO_VALUE": "**버전:** {{version}}\n**자바:** {{jvm}}\n**Lavaplayer:** {{lavaplayer}}\n**Git:** {{git}}\n**빌드 시간:** {{buildTime}}", "NODE_STATUS_MEMORY_TITLE": "📑 메모리", - "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당 가능:** {{allocated}}\n**예약 가능:** {{reservable}}\n", + "NODE_STATUS_MEMORY_VALUE": "**사용중:** {{used}}\n**사용 가능:** {{free}}\n**할당됨:** {{allocated}}\n**예약 가능:** {{reservable}}\n", "NODE_STATUS_PING": "지연시간", "NODE_STATUS_STATS_TITLE": "📊 상태", - "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**음악을 듣는 서버:** {{playerCount}}개\n**음악을 듣는 인원:** {{playingCount}}명", + "NODE_STATUS_STATS_VALUE": "**가동 시간:** {{uptime}}\n**{{pingKey}}:** {{nodePing}}ms\n**플레이어:** {{playerCount}}개\n**재생 중:** {{playingCount}}개", "NODE_STATUS_TITLE": "🛰️ 노드 상태", "NODE_STATUS_TITLE_2": "🛰️ '{{nodeName}}'의 상태", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}의 연결이 끊어졌습니다.", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}}개 노드의 연결이 끊어졌습니다", "PING_DESCRIPTION": "**봇:** {{botPing}}\n**API:** {{apiPing}}ms", "PING_TITLE": "🛰️ 지연시간", "QUEUE_ADD_PLAYLIST": "재생목록 추가", - "QUEUE_ADD_TRACK": "재생목록 추가", + "QUEUE_ADD_TRACK": "음악 추가", "QUEUE_LIST_LOOP_MODE": "반복: {{repeatMode}}", "QUEUE_LIST_TITLE": "재생목록", "REMOVE_LIST_LOOP_MODE": "반복: {{repeatMode}}", - "REMOVE_LIST_TITLE": "제거된 목록", - "REMOVE_TRACK_TITLE": "제거된 목록", + "REMOVE_LIST_TITLE": "제거 목록", + "REMOVE_TRACK_TITLE": "제거된 음악", "SERVER_TITLE": "{{name}}이(가) 가입된 서버", "SERVER_FIELD_DJ_ROLE": "DJ 역할", "SERVER_FIELD_ADMINS": "관리자", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**재생 중:**", "QUEUE_EMPTY": "*대기열에 노래가 없습니다*", "QUEUE_HEADER": "**대기열:**" -} \ No newline at end of file +} diff --git a/src/locales/ko-KR/events.json b/src/locales/ko-KR/events.json index f0fda299..6fe3ba13 100644 --- a/src/locales/ko-KR/events.json +++ b/src/locales/ko-KR/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ 서버 데이터를 캐시에서 가져올 수 없습니다.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ 봇과 같은 음성 채널에 있지 않습니다.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "지정된 음성 채널 <#{{channelId}}>에 참여 후 요청을 해주세요.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ 음성 채널에 연결되어 있지 않습니다.", - "ERROR_NOT_PLAYING": "❌ 현재 재생 중인 음악이 없습니다.", - "ERROR_REQUIRE_ADMIN": "❌ 해당 명령어는 관리자 권한이 필요합니다.", - "ERROR_REQUIRE_DJ": "❌ 이 명령을 사용하려면 DJ 권한이 필요합니다.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ DM을 보낼 수 없습니다.", - "ERROR_SLASH_NOT_ENABLE": "❌ 슬래시 명령이 활성화되어 있지 않습니다.", - "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다.", - "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다.", - "MESSAGE_MUSIC_SHUFFLE": "✅ 음악을 랜덤으로 재생합니다.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ 서버 데이터를 캐시에서 가져올 수 없습니다", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ 봇과 같은 음성 채널에 있지 않습니다", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "지정된 음성 채널 <#{{channelId}}>에 참여 후 요청을 해주세요", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ 음성 채널에 연결되어 있지 않습니다", + "ERROR_NOT_PLAYING": "❌ 현재 재생 중인 음악이 없습니다", + "ERROR_REQUIRE_ADMIN": "❌ 해당 명령어는 관리자 권한이 필요합니다", + "ERROR_REQUIRE_DJ": "❌ 이 명령을 사용하려면 DJ 권한이 필요합니다", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ DM을 보낼 수 없습니다", + "ERROR_SLASH_NOT_ENABLE": "❌ 슬래시 명령이 활성화되어 있지 않습니다", + "ERROR_TIME_EXPIRED": "❌ 시간이 초과되었습니다", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 봇이 떠났습니다", + "MESSAGE_BOT_STOP": "✅ 봇을 멈췄습니다", + "MESSAGE_MUSIC_SHUFFLE": "✅ 재생목록을 섞었습니다", "MESSAGE_NOW_PLAYING_SUBTITLE": "**아티스트:** {{author}}\n**길이:** {{label}}\n", "MESSAGE_NOW_PLAYING_TITLE": "**재생중:** {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 페이지 {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 입력 혹은 선택하세요.", + "MESSAGE_SELECT_LOOP_MODE": "반복 모드를 선택하세요", "LOOP_MODE_OFF": "끄기", "LOOP_MODE_SINGLE": "한 곡 반복", "LOOP_MODE_ALL": "전체 반복", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다.", - "MESSAGE_SET_LOOP_MODE": "✅ `{{mode}}`(으)로 설정되었습니다.", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ DM으로 음악 제목을 보냈습니다", + "MESSAGE_SET_LOOP_MODE": "✅ 반복 모드를 `{{mode}}`(으)로 설정했습니다", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 이 채널에서는 명령을 실행할 수 없습니다. <#{{channelId}}> 채널을 사용하세요." -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/commands.json b/src/locales/ru-RU/commands.json index 72869773..c44d5cbe 100644 --- a/src/locales/ru-RU/commands.json +++ b/src/locales/ru-RU/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Удалить пользователя из чёрного списка", "CONFIG_BLACKLIST_OPTION_LIST": "Показать список пользователей в чёрном списке", "CONFIG_BLACKLIST_OPTION_USER": "Пользователь для добавления или удаления из чёрного списка", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже находится в чёрном списке.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> не внесен(-а) в чёрный список.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> добавлен в чёрный список", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> удалён из чёрного списка", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> уже в чёрном списке", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> нет в чёрном списке", "MESSAGE_BLACKLIST_LIST_TITLE": "Текущий чёрный список", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст.", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Сейчас чёрный список пуст", "CONFIG_CLEAR_DESCRIPTION": "Очистить очередь музыки", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Переместить панель управления вниз", @@ -24,12 +24,12 @@ "CONFIG_FILTER_OPTION_DESCRIPTION": "Выберите музыкальный фильтр для наложения", "CONFIG_HELP_DESCRIPTION": "Получить справку по командам", "CONFIG_HELP_USAGE": "help [команда]", - "CONFIG_HELP_OPTION_DESCRIPTION": "Для какой команды нужна подсказка", + "CONFIG_HELP_OPTION_DESCRIPTION": "Команда, по которой нужна справка", "CONFIG_JOIN_DESCRIPTION": "Подключиться к текущему голосовому каналу", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Изменить язык бота", - "CONFIG_LANG_USAGE": "language <языковой код>", - "CONFIG_LANG_OPTION_DESCRIPTION": "Кодовое обозначение языка", + "CONFIG_LANG_USAGE": "language <доступный язык>", + "CONFIG_LANG_OPTION_DESCRIPTION": "Выберите язык", "CONFIG_LEAVE_DESCRIPTION": "Покинуть текущий голосовой канал", "CONFIG_LEAVE_USAGE": "leave", "CONFIG_LOOP_DESCRIPTION": "Включить или выключить режим повтора музыки", @@ -56,13 +56,13 @@ "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Ссылка или название трека для немедленного воспроизведения", "CONFIG_PLAYLAST_DESCRIPTION": "Повторно воспроизвести текущий или последний трек", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно воспроизводившийся трек не найден.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова добавлен в очередь.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Эту команду можно использовать только при пустой очереди", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Недавно сыгранный трек не найден", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Трек снова в очереди", "CONFIG_QUEUE_DESCRIPTION": "Показать текущий плейлист", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Выбрать трек для удаления из плейлиста", - "CONFIG_REMOVE_USAGE": "remove <номер трека> [начальный индекс конечный индекс]", + "CONFIG_REMOVE_USAGE": "remove <номер трека> [от до]", "CONFIG_REMOVE_OPTION_DESCRIPTION": "Номер трека", "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Диапазон треков", "CONFIG_RESUME_DESCRIPTION": "Продолжить воспроизведение", @@ -70,9 +70,9 @@ "CONFIG_SEARCH_DESCRIPTION": "Введите название трека для поиска", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Название трека", - "CONFIG_SEEK_DESCRIPTION": "Перемотать трек на указанное время", - "CONFIG_SEEK_USAGE": "seek <[чч]мм]сс/[чч:мм]:сс> (пример: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "Целевое время (пример: 3m20s, 1:20:55)", + "CONFIG_SEEK_DESCRIPTION": "Перемотать трек к заданному моменту", + "CONFIG_SEEK_USAGE": "seek <[чч]мм]сс/[чч:мм]:сс> (пример: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Время в треке (пример: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Показать активные серверы", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Перемешать плейлист", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [громкость]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Громкость по умолчанию (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Ошибка команды. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди нет музыки для воспроизведения.", - "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится.", - "ERROR_SKIP_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может её пропустить. Админы и DJ (если включены) тоже могут пропускать.", - "ERROR_SEEK_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может перематывать её. Админы и DJ (если включены) тоже могут перематывать.", - "ERROR_PAUSE_NOT_REQUESTER": "❌ Только пользователь, заказавший эту песню, может поставить её на паузу. Админы и DJ (если включены) тоже могут ставить на паузу.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ В очереди сейчас нет треков", + "ERROR_NO_PLAYING": "❌ Сейчас ничего не воспроизводится", + "ERROR_SKIP_NOT_REQUESTER": "❌ Пропустить этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_SEEK_NOT_REQUESTER": "❌ Перематывать этот трек может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_PAUSE_NOT_REQUESTER": "❌ Ставить этот трек на паузу может только тот, кто его заказал. Админы и DJ (если присутствуют) тоже могут.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Я не могу подключиться к голосовому каналу", "ERROR_PLAY_MUSIC": "❌ Сервис испытывает проблемы, попробуйте позже. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Ничего не найдено. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", - "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена.", - "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев.", - "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев.", - "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя для добавления на роль диджея.", - "MESSAGE_DJ_NO_BOTS": "Нельзя добавлять ботов на роль диджея.", - "MESSAGE_DJ_STATIC_MODE": "Права DJ управляются через конфигурацию в статическом режиме.", - "MESSAGE_DJ_NO_PLAYER": "Активный музыкальный плеер не найден.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем.", - "MESSAGE_DJ_SUCCESS": "Пользователь <@{{userId}}> назначен на роль диджея.", + "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло", + "MESSAGE_CLEAR_SUCCESS": "✅ Очередь очищена", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Панель управления обновлена", + "MESSAGE_DJ_NO_PERMISSION": "Вам нужны права диджея или администратора для добавления диджеев", + "MESSAGE_DJ_ADMIN_ONLY": "Только администраторы могут добавлять или удалять диджеев", + "MESSAGE_DJ_MENTION_USER": "Пожалуйста, упомяните пользователя, которого хотите сделать диджеем", + "MESSAGE_DJ_NO_BOTS": "Ботов нельзя назначать диджеями", + "MESSAGE_DJ_STATIC_MODE": "В статическом режиме права DJ настраиваются только через файл конфигурации", + "MESSAGE_DJ_NO_PLAYER": "Нет плеера, воспроизводящего сейчас музыку", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> уже назначен диджеем", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> назначен диджеем", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ покинул канал! Автоматически выбран новый DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Список диджеев", "MESSAGE_DJ_LIST_ADMINS": "Администраторы", "MESSAGE_DJ_LIST_ROLE_DJS": "Назначенные диджеи", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Ситуативные диджеи", - "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались.", + "MESSAGE_DJ_LIST_NONE": "Диджеи не назначались", "MESSAGE_DJ_LIST_ERROR": "❌ Не удалось загрузить список диджеев. Попробуйте снова.", - "MESSAGE_DJ_ROLE_NOT_SET": "Роль DJ не настроена", - "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден.", - "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр для наложения: ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Выберите применяемый эффект", - "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена.", + "MESSAGE_DJ_ROLE_NOT_SET": "Постоянный DJ не назначен", + "MESSAGE_FILTER_NOT_FOUND": "❌ Эффект не найден", + "MESSAGE_FILTER_SELECT_LIST": "Выберите музыкальный фильтр: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Выберите эффект", + "MESSAGE_HELP_NOT_FOUND": "❌ Команда не найдена", "MESSAGE_HELP_SELECT_LIST": "Выберите команду для получения справки: ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Выберите подсказку", - "MESSAGE_JOIN_SUCCESS": "✅ Бот подключился.", + "MESSAGE_HELP_SELECT_MODE": "Выберите команду", + "MESSAGE_JOIN_SUCCESS": "✅ Подключился к голосовому каналу", "MESSAGE_LANG_ARGS_ERROR": "Неверный языковой код, доступные языки: {{langList}}", "MESSAGE_LANG_AVAILABLE_LIST": "Доступные языки: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Язык успешно изменён на `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал.", + "MESSAGE_LANG_SUCCESS": "✅ Язык изменён на `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Бот покинул канал", "REPEAT_MODE_OFF": "ВЫКЛ", "REPEAT_MODE_SINGLE": "ОДИНОЧНЫЙ", "REPEAT_MODE_ALL": "ВСЕ", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Все", "LOOP_SELECT_PLACEHOLDER": "Выберите режим повтора", "LOOP_SELECT_DESCRIPTION": "Установить режим повтора: {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Режим повтора установлен: `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена.", + "MESSAGE_LOOP_MODE": "✅ Режим повтора теперь `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Очередность треков изменена", "MESSAGE_MOVE_WRONG_INDEX": "❌ Укажите позиции в корректном диапазоне. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nи ещё {{length}} треков", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Сохранить трек", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Сейчас играет: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Сейчас играет: {{title}} | Поставил: {{requester}}", "UNKNOWN_USER": "Неизвестно", - "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название искомой музыки.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Музыка добавлена.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено.", + "MESSAGE_PAUSE_FAIL": "❌ Не удалось поставить музыку на паузу", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Музыка поставлена на паузу", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Музыка приостановлена", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Введите название трека или ссылку", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Трек добавлен в очередь", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Совпадений не найдено", "MESSAGE_PLAY_SELECT_DURATION": "Длительность: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Выберите музыку", - "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено.", - "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить музыку.", - "MESSAGE_REMOVE_INSTRUCTION": "Выберите позицию трека от **1** до **{{length}}** для **удаления** или введите другое значение для отмены. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста.", - "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку.", - "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена.", - "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено.", + "MESSAGE_REMOVE_CANCEL": "✅ Удаление отменено", + "MESSAGE_REMOVE_FAIL": "❌ Не удалось удалить трек", + "MESSAGE_REMOVE_INSTRUCTION": "Выберите номер трека от **1** до **{{length}}**, чтобы **удалить** его, или введите любой другой номер для отмены. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ После текущего трека очередь пуста", + "MESSAGE_RESUME_FAIL": "❌ Не удалось продолжить музыку", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Музыка возобновлена", + "MESSAGE_RESUME_SUCCESS": "▶️ Воспроизведение продолжено", "MESSAGE_SEEK_ARGS_ERROR": "❌ Неверный формат времени.\n(**`пример: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Позиция перемотки превышает длительность трека. (`{{duration}}`)\nПереход к следующей песне.", - "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан.", - "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку.", - "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен.", - "MESSAGE_STOP_SUCCESS": "✅ Музыка остановлена.", + "MESSAGE_SEEK_SKIP": "✅ Указанная позиция (`{{duration}}`) больше длительности трека\nПерехожу к следующему треку", + "MESSAGE_SEEK_SUCCESS": "✅ Музыка перемотана на `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Плейлист перемешан", + "MESSAGE_SKIP_FAIL": "❌ Не удалось пропустить музыку", + "MESSAGE_SKIP_SUCCESS": "✅ Трек пропущен", + "MESSAGE_STOP_SUCCESS": "✅ Воспроизведение остановлено, очередь очищена", "MESSAGE_VOLUME_ARGS_ERROR": "Текущая громкость: **{{volume}}** 🔊\n**Чтобы изменить громкость, введите число от `1` до `{{maxVolume}}`.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Введите число от `1` до `{{maxVolume}}` для изменения громкости.**", - "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\\n**Выберите желаемую громкость:**", - "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена.", + "MESSAGE_VOLUME_SELECT": "Текущая громкость: **{{volume}}%** 🔊\n**Выберите желаемую громкость:**", + "MESSAGE_VOLUME_SAME": "❌ Указанная громкость уже установлена", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Громкость по умолчанию для этого сервера изменена на **{{volume}}%**! (Макс: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию.", - "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас у вас **{{current}}** треков в очереди (лимит: **{{limit}}**). Подождите, пока некоторые треки доиграют.", - "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в очереди **{{total}}** треков (глобальный лимит: **{{limit}}**). Подождите, пока некоторые треки доиграют.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен частично: добавлено **{{added}}** треков, пропущено **{{skipped}}** из-за лимита очереди (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен частично: добавлено **{{added}}** треков, пропущено **{{skipped}}** из-за глобального лимита (**{{total}}/{{limit}}**).", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Текущая громкость по умолчанию: **{{volume}}%** 🔊\nДля изменения введите число от `1` до `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Введите число от `1` до `{{maxVolume}}` для изменения громкости по умолчанию", + "ERROR_QUEUE_LIMIT_REACHED": "❌ Вы достигли лимита очереди! Сейчас в ней **{{current}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", + "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Достигнут глобальный лимит очереди! Сейчас в ней **{{total}}** из **{{limit}}** треков. Подождите, пока часть из них доиграет.", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за лимита очереди (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Плейлист добавлен не полностью — добавлено **{{added}}** треков, а **{{skipped}}** пропущено из-за глобального лимита (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "ID сервера", "MESSAGE_SERVER_GUILD": "Сервер", "MESSAGE_SERVER_MEMBERS": "Участники", @@ -197,73 +197,76 @@ "UNIT_MB": "МБ", "UNIT_GB": "ГБ", "UNIT_TB": "ТБ", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Менеджер чёрного списка не инициализирован.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Чёрный список ещё не загружен, попробуйте позже", "HELP_SELECT_MUSIC_PLACEHOLDER": "Выберите музыкальную команду", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите утилиту", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Выберите служебную команду", "HELP_COMMAND_ALIASES": "Алиасы: {{aliases}}", "HELP_COMMAND_NONE": "Нет", "LABEL_FILTER_CLEAR": "Очистить фильтры", - "CONFIG_PLAYLIST_DESCRIPTION": "Управление плейлистами: сохранение, загрузка, удаление и импорт из M3U.", + "CONFIG_PLAYLIST_DESCRIPTION": "Управление плейлистами: сохранение, загрузка, удаление и импорт из M3U", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Сохранить текущую очередь в плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Загрузить и воспроизвести сохраненный плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Показать список всех плейлистов на сервере.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Показать список треков в плейлисте.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Удалить плейлист.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Сохранить текущую очередь в плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Загрузить и воспроизвести сохраненный плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Показать список всех плейлистов на сервере", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Показать список треков в плейлисте", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Удалить плейлист", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Импортировать плейлист по ссылке на файл M3U", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Удалить трек из плейлиста", "CONFIG_PLAYLIST_OPTION_NAME": "Имя плейлиста", - "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (опционально)", + "CONFIG_PLAYLIST_OPTION_SEARCH": "Название трека для поиска в плейлисте (необязательно)", "CONFIG_PLAYLIST_OPTION_URL": "Ссылка на скачивание файла M3U", "CONFIG_PLAYLIST_OPTION_INDEX": "Номер трека для удаления (начиная с 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Менеджер плейлистов не инициализирован.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Пожалуйста, укажите название плейлиста.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`.", - "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U. (Код ошибки: {{status}})", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Плейлисты пока недоступны, попробуйте позже", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Пожалуйста, введите корректную команду (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Неизвестная подкоманда", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Сейчас ничего не воспроизводится или очередь пуста", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Пожалуйста, укажите название плейлиста", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Не удалось сохранить плейлист", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Плейлист `{{name}}` не найден или пуст", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Трек `{{searchQuery}}` не найден в плейлисте `{{name}}`", + "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Не удалось загрузить файл M3U (код ошибки {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Загрузка M3U заблокирована политикой безопасности. Используйте общедоступный HTTPS URL размером до {{maxSizeMiB}} МиБ, отвечающий в течение {{timeoutSeconds}} секунд.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Плейлист может содержать не более {{limit}} треков", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Ошибка разбора или сохранения M3U плейлиста. Убедитесь, что формат правильный.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Не удалось удалить трек. Убедитесь, что это правильный номер.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Сохранено {{count}} треков из текущей очереди в плейлист `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен. (Добавлено: {{added}}, Пропущено: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Плейлист `{{name}}` сохранён — {{count}} треков из текущей очереди", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Поиск и добавление {{count}} треков из плейлиста `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Плейлист `{{name}}` загружен: добавлено {{added}}, пропущено {{skipped}}", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ На этом сервере нет сохраненных плейлистов", "MESSAGE_PLAYLIST_LIST_TITLE": "Плейлисты сервера", "MESSAGE_PLAYLIST_INFO_TITLE": "Информация о плейлисте: {{name}} ({{count}} треков)", - "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и еще {{remaining}} треков.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` успешно удален.", - "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний плейлист M3U успешно импортирован!\nЗарегистрировано {{count}} треков в плейлисте `{{name}}`.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Удален трек №{{index}} из плейлиста `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста.", + "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*и ещё {{remaining}} треков.*", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Плейлист `{{name}}` удалён", + "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Внешний M3U-плейлист импортирован!\nВ плейлист `{{name}}` добавлено {{count}} треков.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Трек №{{index}} удалён из плейлиста `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Поиск и воспроизведение радиоканала по названию из импортированного плейлиста", "CONFIG_RADIO_USAGE": "radio [название]", "CONFIG_RADIO_OPTION_PLAYLIST": "Название плейлиста для поиска", "CONFIG_RADIO_OPTION_CHANNEL": "Название канала или жанр для воспроизведения (например, workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Использование: `{{prefix}}radio [название плейлиста] [название канала]` (например, `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Укажите название плейлиста и название канала", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ В плейлисте `{{playlistName}}` не найден радиоканал, соответствующий `{{channelQuery}}`.\nДоступные примеры: {{samples}} и др.", - "ERROR_RADIO_NOT_READY": "❌ Плейлист радио не готов.\nСначала импортируйте плейлист радио с помощью `/playlist import {{name}} `.\n(Например: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Радио пока не настроено.\nСначала импортируйте радио-плейлист командой `/playlist import {{name}} `.\nФайл должен быть в формате M3U с тегами EXTINF.", "ERROR_RADIO_NO_MATCH": "❌ Радиоканал по запросу `{{query}}` не найден.\nДоступные варианты: {{samples}} и др.", "ERROR_RADIO_LOAD_FAILED": "❌ Ошибка загрузки радиоканала: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Поиск радиоканала **{{title}}** в плейлисте **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Не удалось загрузить радиопоток (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Поиск радиоканала **{{title}}** в плейлисте **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Начало воспроизведения радиоканала **{{title}}** из плейлиста **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Страница {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Только пользователь, вызвавший команду, может переключать страницы.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Только пользователь, вызвавший команду, может подтвердить это действие.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Только пользователь, вызвавший команду, может переключать страницы", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Только пользователь, вызвавший команду, может подтвердить это действие", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Плейлист уже существует", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "Плейлист с именем **`{{name}}`** (всего {{count}} треков) уже существует.\nХотите перезаписать его импортированным плейлистом?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "Плейлист с именем **`{{name}}`** (всего {{count}} треков) уже существует.\nХотите перезаписать его текущей очередью?", "LABEL_OVERWRITE": "Перезаписать", "LABEL_CANCEL": "Отмена", - "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменен.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Пропущен текущий трек. Очередь пуста." -} \ No newline at end of file + "MESSAGE_PLAYLIST_OVERWRITING": "Перезапись плейлиста..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Импорт плейлиста отменён", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Время подтверждения импорта истекло", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Сохранение плейлиста отменено", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Время ожидания подтверждения сохранения истекло", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Текущий трек пропущен. Очередь пуста.", + "ERROR_PLAYLIST_DISABLED": "❌ Функции плейлистов отключены.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U-плейлисты нельзя добавлять в очередь целиком. Используйте команду `/radio` для выбора радиостанции.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Импорт M3U-плейлистов доступен только администраторам и DJ." +} diff --git a/src/locales/ru-RU/embeds.json b/src/locales/ru-RU/embeds.json index 2052c20d..3ec3a2ee 100644 --- a/src/locales/ru-RU/embeds.json +++ b/src/locales/ru-RU/embeds.json @@ -4,22 +4,22 @@ "DASHBOARD_SUBTITLE": "Автор: **{{author}}**\nДлительность: **{{duration}}**\n────────────────────\nГромкость: `{{volume}}%`\nПовтор: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Заказал:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**Ситуативный DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу.", - "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время некоторые функции окажутся временно недоступны.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Подключился к голосовому каналу", + "MAINTAIN_DESCRIPTION": "Бот перейдёт в режим обслуживания через несколько минут. На это время часть функций будет недоступна.", "MAINTAIN_TITLE": "⚠️ Техническое обслуживание ⚠️", - "MESSAGE_BLACKLIST": "В голосовом канале есть пользователь(и) из чёрного списка.", + "MESSAGE_BLACKLIST": "В голосовом канале есть пользователи из чёрного списка", "MESSAGE_COMMAND": "Команда **{{command}}**", "MESSAGE_FILTER": "Установлен фильтр: **{{effectName}}**", "NODE_ALL_ACTIVE": "Все узлы активны", "NODE_CONNECTED": "Задержка: **{{ping}}мс**", "NODE_DISCONNECTED": "**ОТКЛЮЧЕН**", - "NODE_STATUS_ARGS_ERROR": "❌ Укажите корректное имя узла.", + "NODE_STATUS_ARGS_ERROR": "❌ Укажите корректное имя узла", "NODE_STATUS_CPU_TITLE": "⚙️ ЦП", "NODE_STATUS_CPU_VALUE": "Ядра: **{{cores}}**\nНагрузка системы: **{{systemLoad}}**\nНагрузка Lavalink: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ ИНФОРМАЦИЯ", "NODE_STATUS_INFO_VALUE": "Версия: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nВремя сборки: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 ПАМЯТЬ", - "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nРезервируемо: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Использовано: **{{used}}**\nСвободно: **{{free}}**\nВыделено: **{{allocated}}**\nДоступно: **{{reservable}}**\n", "NODE_STATUS_PING": "Задержка", "NODE_STATUS_STATS_TITLE": "📊 СТАТИСТИКА", "NODE_STATUS_STATS_VALUE": "Время работы: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} мс**\nПлееров: **{{playerCount}}**\nВоспроизводящих: **{{playingCount}}**", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Сейчас играет:**", "QUEUE_EMPTY": "*Нет треков в очереди*", "QUEUE_HEADER": "**Очередь:**" -} \ No newline at end of file +} diff --git a/src/locales/ru-RU/events.json b/src/locales/ru-RU/events.json index fe14dd73..c69a4dda 100644 --- a/src/locales/ru-RU/events.json +++ b/src/locales/ru-RU/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в указанный голосовой канал <#{{channelId}}> и вызовите меня.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу.", - "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится.", - "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора.", - "ERROR_REQUIRE_DJ": "❌ Для этой команды требуются права DJ.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Я не могу отправить вам личное сообщение.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash-команды не включены.", - "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Бот вышел из канала.", - "MESSAGE_BOT_STOP": "✅ Бот остановлен.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Очередь перемешана.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Не удалось получить данные сервера из кэша", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Вы не находитесь в одном голосовом канале со мной", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Пожалуйста, зайдите в голосовой канал <#{{channelId}}> и позовите меня", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Вы не подключены к голосовому каналу", + "ERROR_NOT_PLAYING": "❌ Сейчас ничего не воспроизводится", + "ERROR_REQUIRE_ADMIN": "❌ Для этой команды требуются права администратора", + "ERROR_REQUIRE_DJ": "❌ Для этой команды требуются права DJ", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Я не могу отправить вам личное сообщение", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash-команды не включены", + "ERROR_TIME_EXPIRED": "❌ Время ожидания истекло", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Бот вышел из канала", + "MESSAGE_BOT_STOP": "✅ Бот остановлен", + "MESSAGE_MUSIC_SHUFFLE": "✅ Очередь перемешана", "MESSAGE_NOW_PLAYING_SUBTITLE": "Автор: **{{author}}**\nДлительность: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Сейчас играет: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Страница {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Выберите режим повтора трека.", + "MESSAGE_SELECT_LOOP_MODE": "Выберите режим повтора трека", "LOOP_MODE_OFF": "Выкл", "LOOP_MODE_SINGLE": "Один трек", "LOOP_MODE_ALL": "Все", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения.", - "MESSAGE_SET_LOOP_MODE": "✅ Установлен режим повтора: `{{mode}}`", - "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Вы не можете использовать команды в этом канале. Используйте текстовый канал <#{{channelId}}>" -} \ No newline at end of file + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Я отправил вам название трека в личные сообщения", + "MESSAGE_SET_LOOP_MODE": "✅ Выбран режим повтора: `{{mode}}`", + "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ В этом канале нельзя использовать команды. Пожалуйста, перейдите в текстовый канал <#{{channelId}}>." +} diff --git a/src/locales/sr-RS/commands.json b/src/locales/sr-RS/commands.json index 91fc7d07..a2c49cf4 100644 --- a/src/locales/sr-RS/commands.json +++ b/src/locales/sr-RS/commands.json @@ -1,46 +1,46 @@ { "CONFIG_BLACKLIST_DESCRIPTION": "Upravljaj crnom listom bota", - "CONFIG_BLACKLIST_USAGE": "blacklist [user]", + "CONFIG_BLACKLIST_USAGE": "blacklist [korisnik]", "CONFIG_BLACKLIST_OPTION_SUBCOMMAND": "Radnja koja će se izvršiti", "CONFIG_BLACKLIST_OPTION_ADD": "Dodaj korisnika na crnu listu", "CONFIG_BLACKLIST_OPTION_REMOVE": "Ukloni korisnika sa crne liste", "CONFIG_BLACKLIST_OPTION_LIST": "Prikaži sve korisnike na crnoj listi", "CONFIG_BLACKLIST_OPTION_USER": "Korisnik koji će biti dodat ili uklonjen sa crne liste", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na dinamičnoj crnoj listi.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dinamična crna lista", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Dinamična crna lista je prazna.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> je dodat na crnu listu", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> je uklonjen sa crne liste", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> je već na crnoj listi", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> nije na crnoj listi", + "MESSAGE_BLACKLIST_LIST_TITLE": "Crna lista", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Crna lista je trenutno prazna", "CONFIG_CLEAR_DESCRIPTION": "Obriši svu muziku iz reda", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Premesti kontrolnu tablu na dno", "CONFIG_DASHBOARD_USAGE": "dashboard", "CONFIG_DJ_DESCRIPTION": "Dodaj korisnika kao DJ-a ili prikaži listu DJ-eva", - "CONFIG_DJ_USAGE": "dj [@user]", + "CONFIG_DJ_USAGE": "dj [@korisnik]", "CONFIG_DJ_OPTION_DESCRIPTION": "Korisnik koji će biti dodat kao DJ (opciono)", "CONFIG_FILTER_DESCRIPTION": "Podesi muzički filter", - "CONFIG_FILTER_USAGE": "filter [effect name]", - "CONFIG_FILTER_OPTION_DESCRIPTION": "Izaberi režim muzičkog filtera koji želiš da podesiš", + "CONFIG_FILTER_USAGE": "filter [ime efekta]", + "CONFIG_FILTER_OPTION_DESCRIPTION": "Izaberi muzički filter koji želiš da podesiš", "CONFIG_HELP_DESCRIPTION": "Prikaži pomoć za komande", - "CONFIG_HELP_USAGE": "help [command]", - "CONFIG_HELP_OPTION_DESCRIPTION": "za koju komandu trebaš pomoć", + "CONFIG_HELP_USAGE": "help [komanda]", + "CONFIG_HELP_OPTION_DESCRIPTION": "Komanda za koju trebaš pomoć", "CONFIG_JOIN_DESCRIPTION": "Pridruži se trenutnom govornom kanalu", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Promeni jezik bota", "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Naziv jezika", + "CONFIG_LANG_OPTION_DESCRIPTION": "Izaberi jezik", "CONFIG_LEAVE_DESCRIPTION": "Napusti trenutni govorni kanal", "CONFIG_LEAVE_USAGE": "leave", "CONFIG_LOOP_DESCRIPTION": "Uključi ili isključi režim ponavljanja muzike", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Režim ponavljanja muzike", "CONFIG_MOVE_DESCRIPTION": "Zameni pozicije dve pesme u redu", - "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "Pozicija pesme 1 za zamenu", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Pozicija pesme 2 za zamenu", + "CONFIG_MOVE_USAGE": "move ", + "CONFIG_MOVE_OPTION_DESCRIPTION": "Pozicija pesme 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "Pozicija pesme 2", "CONFIG_NODE_DESCRIPTION": "Prikaži status aktivnih nodova", - "CONFIG_NODE_USAGE": "node [node name]", + "CONFIG_NODE_USAGE": "node [ime noda]", "CONFIG_NODE_OPTION_DESCRIPTION": "Naziv noda", "CONFIG_NOW_PLAYING_DESCRIPTION": "Prikaži trenutno puštenu muziku", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", @@ -49,30 +49,30 @@ "CONFIG_PING_DESCRIPTION": "Prikaži ping servera", "CONFIG_PING_USAGE": "ping", "CONFIG_PLAY_DESCRIPTION": "Unesi link pesme ili naziv pesme za puštanje", - "CONFIG_PLAY_USAGE": "play ", + "CONFIG_PLAY_USAGE": "play ", "CONFIG_PLAY_OPTION_DESCRIPTION": "Link pesme ili naziv pesme", "CONFIG_PLAYFIRST_DESCRIPTION": "Unesi link pesme ili naziv pesme da bi se pustila prva", - "CONFIG_PLAYFIRST_USAGE": "playfirst ", + "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Link pesme ili naziv pesme koja će se pustiti prva", "CONFIG_PLAYLAST_DESCRIPTION": "Ponovo pusti trenutnu ili poslednju puštenu pesmu", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Ova komanda se može koristiti samo kada je red prazan.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Nije pronađena nedavno puštena pesma.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Pesma je ponovo dodata u red.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Ova komanda se može koristiti samo kada je red prazan", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Nije pronađena nedavno puštena pesma", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Pesma je ponovo dodata u red", "CONFIG_QUEUE_DESCRIPTION": "Prikaži trenutnu listu pesama", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Izaberi pesmu za uklanjanje iz liste pesama", - "CONFIG_REMOVE_USAGE": "remove [from index to index 2 track]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "broj indeksa pesme", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "od indeksa do indeksa 2 pesme", + "CONFIG_REMOVE_USAGE": "remove [od do]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Broj pesme", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Raspon pesama", "CONFIG_RESUME_DESCRIPTION": "Nastavi pauziranu pesmu", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "Unesi naziv pesme za pretragu", - "CONFIG_SEARCH_USAGE": "search ", + "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Naziv pesme", "CONFIG_SEEK_DESCRIPTION": "Premotaj pesmu na određeno vreme", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (ex: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "ciljno vreme (npr: 3m20s, 1:20:55)", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (npr: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Ciljno vreme (npr: 165, 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "Prikaži trenutno aktivne servere", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Izmešaj listu pesama", @@ -90,44 +90,44 @@ "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [jačina]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Podrazumevana jačina zvuka (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Greška u komandi. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Trenutno nema muzike za puštanje u redu.", - "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ U redu trenutno nema pesama", + "ERROR_NO_PLAYING": "❌ Trenutno se ne pušta muzika", "ERROR_SKIP_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je preskoči. Administratori i DJ-evi (ako je omogućeno) takođe mogu da preskoče.", "ERROR_SEEK_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je premotava. Administratori i DJ-evi (ako je omogućeno) takođe mogu da premotavaju.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Samo osoba koja je zatražila ovu pesmu može da je pauzira. Administratori i DJ-evi (ako je omogućeno) takođe mogu da pauziraju.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Ne mogu da se pridružim govornom kanalu.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Ne mogu da se pridružim govornom kanalu", "ERROR_PLAY_MUSIC": "❌ Servis ima problema, pokušaj ponovo. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Nema rezultata. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", - "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana.", - "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve.", - "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve.", - "MESSAGE_DJ_MENTION_USER": "Molim te označi korisnika da bi ga dodao kao DJ-a.", - "MESSAGE_DJ_NO_BOTS": "Ne mogu dodati botove kao DJ-eve.", - "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu.", - "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ.", - "MESSAGE_DJ_SUCCESS": "Uspešno dodat <@{{userId}}> kao DJ.", + "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo", + "MESSAGE_CLEAR_SUCCESS": "✅ Red je obrisan", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Kontrolna tabla je ažurirana", + "MESSAGE_DJ_NO_PERMISSION": "Trebaš DJ ili Admin dozvole da bi dodao DJ-eve", + "MESSAGE_DJ_ADMIN_ONLY": "Samo administratori mogu dodati ili ukloniti DJ-eve", + "MESSAGE_DJ_MENTION_USER": "Molim te označi korisnika da bi ga dodao kao DJ-a", + "MESSAGE_DJ_NO_BOTS": "Ne mogu dodati botove kao DJ-eve", + "MESSAGE_DJ_STATIC_MODE": "DJ dozvole se upravljaju putem konfiguracije u STATIC režimu", + "MESSAGE_DJ_NO_PLAYER": "Nije pronađen aktivni muzički plejer", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> je već DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> je dodat kao DJ", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ je napustio kanal! Automatski izabran novi DJ: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 Lista DJ-eva", "MESSAGE_DJ_LIST_ADMINS": "Administratori", - "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi bazirani na ulogama", + "MESSAGE_DJ_LIST_ROLE_DJS": "DJ-evi po ulozi", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamični DJ-evi", - "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva.", + "MESSAGE_DJ_LIST_NONE": "Nema podešenih DJ-eva", "MESSAGE_DJ_LIST_ERROR": "❌ Neuspešno učitavanje liste DJ-eva. Molim te pokušaj ponovo.", "MESSAGE_DJ_ROLE_NOT_SET": "Nije podešena DJ uloga", - "MESSAGE_FILTER_NOT_FOUND": "❌ Naziv efekta nije pronađen.", - "MESSAGE_FILTER_SELECT_LIST": "Izaberi režim muzičkog filtera koji želiš da podesiš. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Izaberi režim filtera", - "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekat nije pronađen", + "MESSAGE_FILTER_SELECT_LIST": "Izaberi muzički filter: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Izaberi efekat", + "MESSAGE_HELP_NOT_FOUND": "❌ Komanda nije pronađena", "MESSAGE_HELP_SELECT_LIST": "Izaberi komandu da bi dobio pomoć. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Izaberi pomoć", - "MESSAGE_JOIN_SUCCESS": "✅ Bot se pridružio.", - "MESSAGE_LANG_ARGS_ERROR": "Parametar jezika je pogrešan, trenutno dostupni jezici: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Trenutno dostupni jezici: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Uspešno promenjen jezik na `{{locale}}`", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot je napustio.", + "MESSAGE_HELP_SELECT_MODE": "Izaberi komandu", + "MESSAGE_JOIN_SUCCESS": "✅ Pridružio sam se glasovnom kanalu", + "MESSAGE_LANG_ARGS_ERROR": "Pogrešan jezički kod. Dostupni jezici: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Dostupni jezici: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Jezik promenjen na `{{locale}}`", + "MESSAGE_LEAVE_SUCCESS": "✅ Napustio sam glasovni kanal", "REPEAT_MODE_OFF": "ISKLJUČENO", "REPEAT_MODE_SINGLE": "POJEDINAČNO", "REPEAT_MODE_ALL": "SVE", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Sve", "LOOP_SELECT_PLACEHOLDER": "Izaberi režim ponavljanja", "LOOP_SELECT_DESCRIPTION": "Postavi režim ponavljanja na {{mode}}", - "MESSAGE_LOOP_MODE": "✅ Postavljeno ponavljanje na `{{mode}}`.", - "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen.", + "MESSAGE_LOOP_MODE": "✅ Režim ponavljanja je sada `{{mode}}`", + "MESSAGE_MOVE_SUCCESS": "✅ Redosled pesama je uspešno zamenjen", "MESSAGE_MOVE_WRONG_INDEX": "❌ Molim te unesi važeći opseg indeksa. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\ni {{length}} drugih pesama", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Sačuvaj pesmu", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Trenutno svira: {{title}} | Zatražio {{requester}}", "UNKNOWN_USER": "Nepoznato", - "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Napiši naziv muzike koju želiš da pretražiš.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Muzika dodata.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja.", + "MESSAGE_PAUSE_FAIL": "❌ Pauziranje muzike nije uspelo", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Muzika je pauzirana", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Muzika pauzirana", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Unesi naziv pesme ili link", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Pesma dodata u red", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Nema podudaranja", "MESSAGE_PLAY_SELECT_DURATION": "Trajanje: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Izaberi muziku", - "MESSAGE_REMOVE_CANCEL": "✅ Otkazano uklanjanje.", - "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo.", - "MESSAGE_REMOVE_INSTRUCTION": "Izaberi pesmu od **1** do **{{length}}** za **uklanjanje** ili unesi bilo šta drugo za otkazivanje izbora. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Nema muzike u redu nakon trenutne.", - "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ Muzika je nastavljena.", - "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena.", + "MESSAGE_REMOVE_CANCEL": "✅ Uklanjanje otkazano", + "MESSAGE_REMOVE_FAIL": "❌ Uklanjanje muzike nije uspelo", + "MESSAGE_REMOVE_INSTRUCTION": "Unesi broj pesme od **1** do **{{length}}** za **uklanjanje**, ili bilo koji drugi broj za otkazivanje. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Red je prazan nakon trenutne pesme", + "MESSAGE_RESUME_FAIL": "❌ Nastavljanje muzike nije uspelo", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Muzika je nastavljena", + "MESSAGE_RESUME_SUCCESS": "▶️ Muzika nastavljena", "MESSAGE_SEEK_ARGS_ERROR": "❌ Nevažeći format za ciljno vreme.\n(**`npr: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Pozicija premotavanja je izvan trajanja ove pesme. (`{{duration}}`)\nPreskakanje na sledeću pesmu.", - "MESSAGE_SEEK_SUCCESS": "✅ Muzika je premotana na `{{duration}}`.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Muzika izmešana.", - "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo.", - "MESSAGE_SKIP_SUCCESS": "✅ Muzika preskočena.", - "MESSAGE_STOP_SUCCESS": "✅ Muzika zaustavljena.", - "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Da bi promenio jačinu, unesi broj između `1` i `{{maxVolume}}`.**", - "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Unesi broj od `1` do `{{maxVolume}}` da bi promenio jačinu.**", - "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\\n**Molim te izaberi jačinu koju želiš da podesiš:**", - "MESSAGE_VOLUME_SAME": "❌ Jačina koju želiš da podesiš je već trenutna jačina.", + "MESSAGE_SEEK_SKIP": "✅ Navedena pozicija (`{{duration}}`) je duža od trajanja pesme.\nPrelazim na sledeću pesmu.", + "MESSAGE_SEEK_SUCCESS": "✅ Premotano na `{{duration}}`", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Lista pesama izmešana", + "MESSAGE_SKIP_FAIL": "❌ Preskakanje muzike nije uspelo", + "MESSAGE_SKIP_SUCCESS": "✅ Pesma preskočena", + "MESSAGE_STOP_SUCCESS": "✅ Reprodukcija zaustavljena, red obrisan", + "MESSAGE_VOLUME_ARGS_ERROR": "Trenutna jačina: **{{volume}}** 🔊\n**Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", + "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Za promenu jačine unesi broj od `1` do `{{maxVolume}}`.**", + "MESSAGE_VOLUME_SELECT": "Trenutna jačina: **{{volume}}%** 🔊\n**Molim te izaberi jačinu koju želiš da podesiš:**", + "MESSAGE_VOLUME_SAME": "❌ Ta jačina je već podešena", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Podrazumevana jačina zvuka je promenjena na **{{volume}}%** za ovaj server! (Maks: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nDa bi promenio, unesi broj između `1` i `{{maxVolume}}`.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Unesi broj od `1` do `{{maxVolume}}` da bi promenio podrazumevanu jačinu.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Trenutna podrazumevana jačina: **{{volume}}%** 🔊\nZa promenu unesi broj od `1` do `{{maxVolume}}`", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Za promenu podrazumevane jačine unesi broj od `1` do `{{maxVolume}}`", "ERROR_QUEUE_LIMIT_REACHED": "❌ Dostigao si limit reda! Trenutno imaš **{{current}}** pesama u redu (limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Dostignut je globalni limit reda! Red trenutno ima **{{total}}** pesama (globalni limit: **{{limit}}**). Molim te sačekaj da se neke pesme završe pre nego što dodaš još.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog globalnog limita reda (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog tvog limita reda (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Plejlista je delimično dodata: dodato je **{{added}}** pesama, **{{skipped}}** pesama je preskočeno zbog globalnog limita reda (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "ID servera", "MESSAGE_SERVER_GUILD": "Server", "MESSAGE_SERVER_MEMBERS": "Članovi", @@ -192,78 +192,81 @@ "UNIT_HOURS": "č", "UNIT_MINUTES": "m", "UNIT_SECONDS": "s", - "UNIT_BYTES": "Bajta", + "UNIT_BYTES": "Bajt", "UNIT_KB": "KB", "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Blacklist manager nije inicijalizovan.", - "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi Muzičku komandu", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberi Utility komandu", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Crna lista još nije učitana. Pokušaj ponovo kasnije.", + "HELP_SELECT_MUSIC_PLACEHOLDER": "Izaberi muzičku komandu", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Izaberite servisnu komandu", "HELP_COMMAND_ALIASES": "Aliasi: {{aliases}}", "HELP_COMMAND_NONE": "Nijedan", "LABEL_FILTER_CLEAR": "Obriši filtere", - "CONFIG_PLAYLIST_DESCRIPTION": "Sačuvaj, učitaj, uredi plejliste ili ih uvezi iz M3U URL-a.", + "CONFIG_PLAYLIST_DESCRIPTION": "Sačuvaj, učitaj, uredi plejliste ili ih uvezi iz M3U URL-a", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Sačuvaj trenutni red za reprodukciju kao plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Učitaj i pusti sačuvanu plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Prikaži sve plejliste na serveru.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Prikaži pesme u plejlisti.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Obriši plejlistu.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uvezi plejlistu sa udaljenog M3U fajla.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Ukloni pesmu iz plejliste.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Sačuvaj trenutni red za reprodukciju kao plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Učitaj i pusti sačuvanu plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Prikaži sve plejliste na serveru", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Prikaži pesme u plejlisti", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Obriši plejlistu", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uvezi plejlistu sa udaljenog M3U fajla", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Ukloni pesmu iz plejliste", "CONFIG_PLAYLIST_OPTION_NAME": "Ime plejliste", "CONFIG_PLAYLIST_OPTION_SEARCH": "Naziv određene pesme za pretragu u plejlisti (Opciono)", "CONFIG_PLAYLIST_OPTION_URL": "Link za preuzimanje M3U fajla", "CONFIG_PLAYLIST_OPTION_INDEX": "Indeks pesme za uklanjanje (počinje od 1)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Menadžer plejlisti nije inicijalizovan.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Molimo unesite ispravnu potkomandu (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Nepoznata potkomanda.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Muzika se ne pušta ili je red prazan.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Molimo navedite ime plejliste.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Neuspešno čuvanje plejliste.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ Plejlista `{{name}}` nije pronađena ili je prazna.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Pesma `{{searchQuery}}` nije pronađena u plejlisti `{{name}}`.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Menadžer plejlisti nije inicijalizovan", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Molimo unesite ispravnu potkomandu (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Nepoznata potkomanda", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Muzika se ne pušta ili je red prazan", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Molimo navedite ime plejliste", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Neuspešno čuvanje plejliste", + "ERROR_PLAYLIST_NOT_FOUND": "❌ Plejlista `{{name}}` nije pronađena ili je prazna", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ Pesma `{{searchQuery}}` nije pronađena u plejlisti `{{name}}`", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ Preuzimanje M3U fajla nije uspelo. (HTTP status: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ Preuzimanje M3U fajla je blokirano bezbednosnim pravilima. Koristite javni HTTPS URL do {{maxSizeMiB}} MiB koji odgovara u roku od {{timeoutSeconds}} sekundi.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Plejlista može sadržati najviše {{limit}} pesama.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Plejlista može sadržati najviše {{limit}} pesama", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ Neuspešno parsiranje i čuvanje M3U plejliste. Proverite da li je ispravno formatirana.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Uklanjanje pesme nije uspelo. Proverite da li je indeks ispravan.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana. (Uspešno: {{added}}, Preskočeno: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Sačuvano {{count}} pesama iz trenutnog reda u plejlistu `{{name}}`", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 Pretraga i dodavanje {{count}} pesama iz plejliste `{{name}}`..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 Plejlista `{{name}}` je učitana: dodato {{added}}, preskočeno {{skipped}}", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Nema sačuvanih plejlisti na ovom serveru", "MESSAGE_PLAYLIST_LIST_TITLE": "Plejliste na serveru", "MESSAGE_PLAYLIST_INFO_TITLE": "Informacije o plejlisti: {{name}} ({{count}} pesama)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*i još {{remaining}} pesama.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Plejlista `{{name}}` je obrisana.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ Plejlista `{{name}}` je obrisana", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Eksterna M3U plejlista je uvezena!\nRegistrovano {{count}} pesama u plejlisti `{{name}}`.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Uklonjena pesma #{{index}} iz plejliste `{{name}}`.", - "CONFIG_RADIO_DESCRIPTION": "Pretraži i pusti određeni žanr/kanal sa uvezene radio plejliste.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ Uklonjena pesma #{{index}} iz plejliste `{{name}}`", + "CONFIG_RADIO_DESCRIPTION": "Pretraži i pusti određeni žanr/kanal sa uvezene radio plejliste", "CONFIG_RADIO_USAGE": "radio [kanal]", "CONFIG_RADIO_OPTION_PLAYLIST": "Naziv plejliste za pretragu", "CONFIG_RADIO_OPTION_CHANNEL": "Radio kanal ili žanr za puštanje (npr. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Upotreba: `{{prefix}}radio [naziv plejliste] [naziv kanala]` (npr. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Navedite naziv plejliste i naziv kanala", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ Radio kanal koji odgovara `{{channelQuery}}` u plejlisti `{{playlistName}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", - "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\n(npr., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radio plejlista nije spremna.\nPrvo uvezite radio plejlistu pomoću `/playlist import {{name}} `.\nFajl mora biti u M3U formatu sa EXTINF tagovima.", "ERROR_RADIO_NO_MATCH": "❌ Radio kanal koji odgovara `{{query}}` nije pronađen.\nDostupni primeri: {{samples}} itd.", "ERROR_RADIO_LOAD_FAILED": "❌ Greška pri učitavanju radio kanala: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 Pretraživanje radio kanala **{{title}}** u plejlisti **{{playlist}}**...", + "ERROR_RADIO_STREAM_FAILED": "❌ Nije moguće učitati ili pronaći radio strim (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 Pretraživanje radio kanala **{{title}}** u plejlisti **{{playlist}}**..", "MESSAGE_RADIO_PLAYING": "📻 Započeto puštanje radio kanala **{{title}}** sa plejliste **{{playlist}}**!", "MESSAGE_FOOTER_PAGE": "Strana {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Samo korisnik koji je pokrenuo komandu može menjati stranice.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Samo korisnik koji je pokrenuo komandu može potvrditi ovu akciju.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Samo korisnik koji je pokrenuo komandu može menjati stranice", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Samo korisnik koji je pokrenuo komandu može potvrditi ovu akciju", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Plejlista već postoji", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "Plejlista sa imenom **`{{name}}`** (ukupno {{count}} pesama) već postoji.\nDa li želite da je zamenite uvezenom plejlistom?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "Plejlista pod nazivom **`{{name}}`** ({{count}} pesama) već postoji.\nDa li želite da je zamenite trenutnim redom pesama?", "LABEL_OVERWRITE": "Zameni", "LABEL_CANCEL": "Otkaži", - "MESSAGE_PLAYLIST_OVERWRITING": "Zamenjivanje plejliste...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Uvoz plejliste je otkazan.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan." -} \ No newline at end of file + "MESSAGE_PLAYLIST_OVERWRITING": "Zamenjivanje plejliste..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Uvoz plejliste je otkazan", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ Vreme za potvrdu uvoza je isteklo", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Čuvanje plejliste je otkazano", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Potvrda čuvanja je istekla", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Preskočena trenutna pesma. Red je sada prazan.", + "ERROR_PLAYLIST_DISABLED": "❌ Funkcije plejliste su trenutno onemogućene.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U plejliste se ne mogu staviti u red u celosti. Koristite komandu `/radio` za izbor stanice.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ Uvoz M3U plejlisti je ograničen samo na admine i DJ-eve." +} diff --git a/src/locales/sr-RS/embeds.json b/src/locales/sr-RS/embeds.json index 8f52cec4..15b7831c 100644 --- a/src/locales/sr-RS/embeds.json +++ b/src/locales/sr-RS/embeds.json @@ -4,25 +4,25 @@ "DASHBOARD_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{duration}}**\n────────────────────\nJačina zvuka: `{{volume}}%`\nPonavljanje: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Zatražio:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Uspešno povezan sa govornim kanalom.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Uspešno povezan sa govornim kanalom", "MAINTAIN_DESCRIPTION": "Bot će uskoro ući u održavanje. Tokom tog vremena, neke funkcije mogu biti privremeno nedostupne.", "MAINTAIN_TITLE": "⚠️ Obaveštenje o održavanju ⚠️", - "MESSAGE_BLACKLIST": "Postoje korisnici na crnoj listi u govornom kanalu.", + "MESSAGE_BLACKLIST": "Postoje korisnici na crnoj listi u govornom kanalu", "MESSAGE_COMMAND": "Komanda **{{command}}**", "MESSAGE_FILTER": "Režim filtera postavljen na **{{effectName}}**", "NODE_ALL_ACTIVE": "Svi nodovi su aktivni", "NODE_CONNECTED": "Kašnjenje: **{{ping}}ms**", "NODE_DISCONNECTED": "**ISKLJUČEN**", - "NODE_STATUS_ARGS_ERROR": "❌ Molim te unesi važeći naziv noda.", + "NODE_STATUS_ARGS_ERROR": "❌ Molim te unesi važeći naziv noda", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Jezgra: **{{cores}}**\nSistemsko opterećenje: **{{systemLoad}}**\nLavalink opterećenje: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ INFO", "NODE_STATUS_INFO_VALUE": "Verzija: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nVreme izgradnje: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 MEMORIJA", - "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nRezervisano: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Iskorišćeno: **{{used}}**\nSlobodno: **{{free}}**\nDodeljeno: **{{allocated}}**\nDostupno: **{{reservable}}**\n", "NODE_STATUS_PING": "Kašnjenje", "NODE_STATUS_STATS_TITLE": "📊 STATISTIKA", - "NODE_STATUS_STATS_VALUE": "vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "Vreme rada: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlejer: **{{playerCount}}**\nAktivni plejeri: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ Status nodova", "NODE_STATUS_TITLE_2": "🛰️ Status noda '{{nodeName}}'", "NODE_UNHEALTHY": "⚠️ Ima {{unhealthValue}} isključenih nodova", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Trenutno svira:**", "QUEUE_EMPTY": "*Nema pesama u redu*", "QUEUE_HEADER": "**Red:**" -} \ No newline at end of file +} diff --git a/src/locales/sr-RS/events.json b/src/locales/sr-RS/events.json index 80e46384..d0b758f7 100644 --- a/src/locales/sr-RS/events.json +++ b/src/locales/sr-RS/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ Ne mogu da dobijem podatke servera iz keša.", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Nisi na istom govornom kanalu kao ja.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Molim te pridruži se određenom govornom kanalu <#{{channelId}}> i pozovi me.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Nisi povezan na govorni kanal.", - "ERROR_NOT_PLAYING": "❌ Trenutno se ne pušta muzika.", - "ERROR_REQUIRE_ADMIN": "❌ Ova komanda zahteva administratorske privilegije.", - "ERROR_REQUIRE_DJ": "❌ Ova komanda zahteva DJ privilegije.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Ne mogu da ti pošaljem privatnu poruku.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash komanda nije omogućena.", - "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal.", - "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Muzika izmešana.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Ne mogu da dobijem podatke servera iz keša", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Nisi na istom govornom kanalu kao ja", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Molim te pridruži se određenom govornom kanalu <#{{channelId}}> i pozovi me", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Nisi povezan na govorni kanal", + "ERROR_NOT_PLAYING": "❌ Trenutno se ne pušta muzika", + "ERROR_REQUIRE_ADMIN": "❌ Ova komanda zahteva administratorske privilegije", + "ERROR_REQUIRE_DJ": "❌ Ova komanda zahteva DJ privilegije", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Ne mogu da ti pošaljem privatnu poruku", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash komanda nije omogućena", + "ERROR_TIME_EXPIRED": "❌ Vreme je isteklo", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot je napustio kanal", + "MESSAGE_BOT_STOP": "✅ Bot je zaustavljen", + "MESSAGE_MUSIC_SHUFFLE": "✅ Red je izmešan", "MESSAGE_NOW_PLAYING_SUBTITLE": "Autor: **{{author}}**\nTrajanje: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Trenutno svira: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Strana {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Izaberi režim ponavljanja pesme.", + "MESSAGE_SELECT_LOOP_MODE": "Izaberi režim ponavljanja pesme", "LOOP_MODE_OFF": "Isključeno", "LOOP_MODE_SINGLE": "Jedna", "LOOP_MODE_ALL": "Sve", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci.", - "MESSAGE_SET_LOOP_MODE": "✅ Ponavljanje postavljeno na `{{mode}}`", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Poslao sam ti naziv muzike u privatnoj poruci", + "MESSAGE_SET_LOOP_MODE": "✅ Izabran režim ponavljanja: `{{mode}}`", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Ne možeš izvršavati komande u ovom kanalu. Molim te koristi <#{{channelId}}> tekstualni kanal." -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/commands.json b/src/locales/tr-TR/commands.json index 775b7af1..b3a98e7b 100644 --- a/src/locales/tr-TR/commands.json +++ b/src/locales/tr-TR/commands.json @@ -6,12 +6,12 @@ "CONFIG_BLACKLIST_OPTION_REMOVE": "Kullanıcıyı kara listeden çıkar", "CONFIG_BLACKLIST_OPTION_LIST": "Kara listedeki tüm kullanıcıları listele", "CONFIG_BLACKLIST_OPTION_USER": "Kara listeye eklenecek veya çıkarılacak kullanıcı", - "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi.", - "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı.", - "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede.", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> dinamik kara listede değil.", - "MESSAGE_BLACKLIST_LIST_TITLE": "Dinamik Kara Liste", - "MESSAGE_BLACKLIST_LIST_EMPTY": "Dinamik kara liste boş.", + "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> kara listeye eklendi", + "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> kara listeden çıkarıldı", + "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> zaten kara listede", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> kara listede değil", + "MESSAGE_BLACKLIST_LIST_TITLE": "Kara Liste", + "MESSAGE_BLACKLIST_LIST_EMPTY": "Kara liste şu anda boş", "CONFIG_CLEAR_DESCRIPTION": "Kuyruktaki tüm parçaları temizle", "CONFIG_CLEAR_USAGE": "clear", "CONFIG_DASHBOARD_DESCRIPTION": "Dashboard'u alta al", @@ -22,112 +22,112 @@ "CONFIG_FILTER_DESCRIPTION": "Müzik filtresini belirleme", "CONFIG_FILTER_USAGE": "filter [efekt adı]", "CONFIG_FILTER_OPTION_DESCRIPTION": "Belirlemek istediğiniz filtreyi seçin", - "CONFIG_HELP_DESCRIPTION": "Yardım komutunu kullanma", + "CONFIG_HELP_DESCRIPTION": "Komutlar hakkında yardım al", "CONFIG_HELP_USAGE": "help [komut]", - "CONFIG_HELP_OPTION_DESCRIPTION": "yardıma ihtiyaç duyduğunuz komut", + "CONFIG_HELP_OPTION_DESCRIPTION": "Hakkında yardım almak istediğiniz komut", "CONFIG_JOIN_DESCRIPTION": "Seçilen ses kanalına bağlan", "CONFIG_JOIN_USAGE": "join", "CONFIG_LANG_DESCRIPTION": "Bot'un dilini değiştir", "CONFIG_LANG_USAGE": "language ", - "CONFIG_LANG_OPTION_DESCRIPTION": "Yerelleştirme kodu", + "CONFIG_LANG_OPTION_DESCRIPTION": "Bir dil seçin", "CONFIG_LEAVE_DESCRIPTION": "Ses kanalını terket", "CONFIG_LEAVE_USAGE": "leave", - "CONFIG_LOOP_DESCRIPTION": "Döngü modunu açma", + "CONFIG_LOOP_DESCRIPTION": "Döngü modunu aç veya kapat", "CONFIG_LOOP_USAGE": "loop ", "CONFIG_LOOP_OPTION_DESCRIPTION": "Döngü modu", "CONFIG_MOVE_DESCRIPTION": "Kuyruktaki 2 parçanın sırasını değiştir", "CONFIG_MOVE_USAGE": "move ", - "CONFIG_MOVE_OPTION_DESCRIPTION": "1. sıraya alındı", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "2. sıraya alındı", - "CONFIG_NODE_DESCRIPTION": "Aktif bağlantı noktalarının adını göster", - "CONFIG_NODE_USAGE": "node [bağlantı noktası adı]", - "CONFIG_NODE_OPTION_DESCRIPTION": "Bağlantı noktası adı", + "CONFIG_MOVE_OPTION_DESCRIPTION": "1. parçanın konumu", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "2. parçanın konumu", + "CONFIG_NODE_DESCRIPTION": "Aktif düğümlerin durumunu göster", + "CONFIG_NODE_USAGE": "node [düğüm adı]", + "CONFIG_NODE_OPTION_DESCRIPTION": "Düğüm adı", "CONFIG_NOW_PLAYING_DESCRIPTION": "Şuan çalan parçayı göster", "CONFIG_NOW_PLAYING_USAGE": "nowplaying", "CONFIG_PAUSE_DESCRIPTION": "Şuan çalan parçayı durdur", "CONFIG_PAUSE_USAGE": "pause", - "CONFIG_PING_DESCRIPTION": "Sunucu gecikmesi", + "CONFIG_PING_DESCRIPTION": "Sunucu gecikmesini öğren", "CONFIG_PING_USAGE": "ping", "CONFIG_PLAY_DESCRIPTION": "Çalınacak parçanın adını ya da linkini girin", "CONFIG_PLAY_USAGE": "play ", - "CONFIG_PLAY_OPTION_DESCRIPTION": "Parçanın linki ya da adını girin", + "CONFIG_PLAY_OPTION_DESCRIPTION": "Parçanın linki ya da adı", "CONFIG_PLAYFIRST_DESCRIPTION": "Kuyrukta başa eklenecek parçanın adını ya da linkini girin", "CONFIG_PLAYFIRST_USAGE": "playfirst ", "CONFIG_PLAYFIRST_OPTION_DESCRIPTION": "Kuyrukta başa alınacak parçanın linki ya da adı", "CONFIG_PLAYLAST_DESCRIPTION": "Mevcut veya en son çalınan parçayı tekrar çal", "CONFIG_PLAYLAST_USAGE": "playlast", - "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Bu komut yalnızca kuyruk boşken kullanılabilir.", - "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Son çalınan parça bulunamadı.", - "MESSAGE_PLAYLAST_SUCCESS": "✅ Parça kuyruğa tekrar eklendi.", + "ERROR_PLAYLAST_QUEUE_NOT_EMPTY": "❌ Bu komut yalnızca kuyruk boşken kullanılabilir", + "ERROR_PLAYLAST_NO_LAST_TRACK": "❌ Son çalınan parça bulunamadı", + "MESSAGE_PLAYLAST_SUCCESS": "✅ Parça kuyruğa tekrar eklendi", "CONFIG_QUEUE_DESCRIPTION": "Şuanki oynatma listesini göster", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "Oynatma kuyruğundan silmek için bir parça seçin", - "CONFIG_REMOVE_USAGE": "remove [baştan sona kadar 2 parça]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "parça sırası", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "baştan sona kadar 2 parça", + "CONFIG_REMOVE_USAGE": "remove [başlangıç bitiş]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "Parça numarası", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "Parça aralığı", "CONFIG_RESUME_DESCRIPTION": "Duraklatılan parçayı devam ettir", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "Aramak istediğiniz parçanın adını girin", "CONFIG_SEARCH_USAGE": "search ", "CONFIG_SEARCH_OPTION_DESCRIPTION": "Parça adı", "CONFIG_SEEK_DESCRIPTION": "Parçada belirlenen bir zamana atlama", - "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (örn: 3m20s, 1:20:55)", - "CONFIG_SEEK_OPTION_DESCRIPTION": "hedeflenen zaman (örn: 3m20s, 1:20:55)", - "CONFIG_SERVER_DESCRIPTION": "Aktif olan sunucular", + "CONFIG_SEEK_USAGE": "seek <[hh]mm]ss/[hh:mm]:ss> (örn: 165, 3m20s, 1:20:55)", + "CONFIG_SEEK_OPTION_DESCRIPTION": "Hedef zaman (örn: 165, 3m20s, 1:20:55)", + "CONFIG_SERVER_DESCRIPTION": "Aktif sunucuları göster", "CONFIG_SERVER_USAGE": "server", "CONFIG_SHUFFLE_DESCRIPTION": "Oynatma listesini karıştır", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "Parçayı atlama", "CONFIG_SKIP_USAGE": "skip", - "CONFIG_STATUS_DESCRIPTION": "Bot'un durumunu öğrenme", + "CONFIG_STATUS_DESCRIPTION": "Bot'un durumunu göster", "CONFIG_STATUS_USAGE": "status", "CONFIG_STOP_DESCRIPTION": "Şu an çalan müziği ve kuyruğu temizle ancak kanaldan ayrılma", "CONFIG_STOP_USAGE": "stop", - "CONFIG_VOLUME_DESCRIPTION": "Bot'un ses sesviyesini belirleme", + "CONFIG_VOLUME_DESCRIPTION": "Bot'un ses seviyesini değiştir", "CONFIG_VOLUME_USAGE": "v <0-100>", "CONFIG_VOLUME_OPTION_DESCRIPTION": "Ses seviyesi", "CONFIG_VOLUME_DEFAULT_DESCRIPTION": "Bu sunucu için varsayılan ses seviyesini değiştir", "CONFIG_VOLUME_DEFAULT_USAGE": "volume-default [ses-seviyesi]", "CONFIG_VOLUME_DEFAULT_OPTION_DESCRIPTION": "Belirlenecek varsayılan ses seviyesi (1-{{maxVolume}})", "ERROR_LOOP_COMMAND": "❌ Komut hatası. {{command}}", - "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta çalınacak başka bir parça bulunmuyor.", - "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", + "ERROR_NO_MUSIC_IN_QUEUE": "❌ Kuyrukta şu anda parça yok", + "ERROR_NO_PLAYING": "❌ Şuan çalan bir parça bulunmuyor", "ERROR_SKIP_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi atlayabilir. Yöneticiler ve DJ'ler (etkinse) de atlayabilir.", "ERROR_SEEK_NOT_REQUESTER": "❌ Bu şarkıda yalnızca isteyen kişi zaman atlayabilir. Yöneticiler ve DJ'ler (etkinse) de zaman atlayabilir.", "ERROR_PAUSE_NOT_REQUESTER": "❌ Bu şarkıyı sadece isteyen kişi duraklatabilir. Yöneticiler ve DJ'ler (etkinse) de duraklatabilir.", - "ERROR_PLAY_JOIN_CHANNEL": "❌ Ses kanalına giriş yapamıyorum.", + "ERROR_PLAY_JOIN_CHANNEL": "❌ Ses kanalına giriş yapamıyorum", "ERROR_PLAY_MUSIC": "❌ Sistemimiz şuan sorun yaşıyor. Lütfen birazdan tekrar deneyiniz. ({{reason}})", "ERROR_PLAY_SEARCH": "❌ Herhangi bir sonuç bulunamadı. ({{reason}})", - "ERROR_TIME_EXPIRED": "❌ Süre doldu.", - "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi.", - "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi.", - "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var.", - "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir.", - "MESSAGE_DJ_MENTION_USER": "DJ olarak eklemek için bir kullanıcıdan bahsedin.", - "MESSAGE_DJ_NO_BOTS": "Botlar DJ olarak eklenemez.", - "MESSAGE_DJ_STATIC_MODE": "DJ izinleri STATIC modda konfigürasyon üzerinden yönetilir.", - "MESSAGE_DJ_NO_PLAYER": "Aktif müzik çalar bulunamadı.", - "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> zaten bir DJ.", - "MESSAGE_DJ_SUCCESS": "<@{{userId}}> başarıyla DJ olarak eklendi.", + "ERROR_TIME_EXPIRED": "❌ Süre doldu", + "MESSAGE_CLEAR_SUCCESS": "✅ Kuyruk temizlendi", + "MESSAGE_DASHBOARD_SUCCESS": "✅ Arayüz güncellendi", + "MESSAGE_DJ_NO_PERMISSION": "DJ eklemek için DJ veya Yönetici izinlerine ihtiyacınız var", + "MESSAGE_DJ_ADMIN_ONLY": "Sadece yöneticiler DJ ekleyebilir veya kaldırabilir", + "MESSAGE_DJ_MENTION_USER": "DJ olarak eklemek için bir kullanıcıdan bahsedin", + "MESSAGE_DJ_NO_BOTS": "Botlar DJ olarak eklenemez", + "MESSAGE_DJ_STATIC_MODE": "DJ izinleri STATIC modda konfigürasyon üzerinden yönetilir", + "MESSAGE_DJ_NO_PLAYER": "Aktif müzik çalar bulunamadı", + "MESSAGE_DJ_ALREADY_DJ": "<@{{userId}}> zaten bir DJ", + "MESSAGE_DJ_SUCCESS": "<@{{userId}}> başarıyla DJ olarak eklendi", "MESSAGE_DJ_AUTO_SELECT": "🎵 DJ kanaldan ayrıldı! Yeni DJ otomatik olarak seçildi: <@{{userId}}>", "MESSAGE_DJ_LIST_TITLE": "📋 DJ Listesi", "MESSAGE_DJ_LIST_ADMINS": "Yöneticiler", "MESSAGE_DJ_LIST_ROLE_DJS": "Rol tabanlı DJ'ler", "MESSAGE_DJ_LIST_DYNAMIC_DJS": "Dinamik DJ'ler", - "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok.", + "MESSAGE_DJ_LIST_NONE": "Yapılandırılmış DJ yok", "MESSAGE_DJ_LIST_ERROR": "❌ DJ listesi yüklenemedi. Lütfen tekrar deneyin.", "MESSAGE_DJ_ROLE_NOT_SET": "DJ rolü yapılandırılmamış", - "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt adı bulunamadı.", - "MESSAGE_FILTER_SELECT_LIST": "Belirlemek istediğiniz filtreyi seçin. ⬇️", - "MESSAGE_FILTER_SELECT_MODE": "Filtre modunu seçin", - "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı.", + "MESSAGE_FILTER_NOT_FOUND": "❌ Efekt bulunamadı", + "MESSAGE_FILTER_SELECT_LIST": "Bir müzik filtresi seçin: ⬇️", + "MESSAGE_FILTER_SELECT_MODE": "Bir efekt seçin", + "MESSAGE_HELP_NOT_FOUND": "❌ Komut bulunamadı", "MESSAGE_HELP_SELECT_LIST": "Yardım almak için bir komut seçin. ⬇️", - "MESSAGE_HELP_SELECT_MODE": "Yardımı seçin", - "MESSAGE_JOIN_SUCCESS": "✅ Bot Katıldı.", - "MESSAGE_LANG_ARGS_ERROR": "Dil parametresi yanlış, şu anda mevcut diller: {{langList}}", - "MESSAGE_LANG_AVAILABLE_LIST": "Şu anda mevcut diller: {{langList}}", - "MESSAGE_LANG_SUCCESS": "✅ Dil başarıyla `{{locale}}` olarak değiştirildi", - "MESSAGE_LEAVE_SUCCESS": "✅ Bot ayrıldı.", + "MESSAGE_HELP_SELECT_MODE": "Bir komut seçin", + "MESSAGE_JOIN_SUCCESS": "✅ Ses kanalına katıldım", + "MESSAGE_LANG_ARGS_ERROR": "Yanlış dil kodu. Mevcut diller: {{langList}}", + "MESSAGE_LANG_AVAILABLE_LIST": "Mevcut diller: {{langList}}", + "MESSAGE_LANG_SUCCESS": "✅ Dil `{{locale}}` olarak değiştirildi", + "MESSAGE_LEAVE_SUCCESS": "✅ Ses kanalından ayrıldım", "REPEAT_MODE_OFF": "KAPALI", "REPEAT_MODE_SINGLE": "TEK", "REPEAT_MODE_ALL": "HEPSİ", @@ -136,8 +136,8 @@ "LOOP_CHOICE_ALL": "Hepsi", "LOOP_SELECT_PLACEHOLDER": "Döngü modunu seçin", "LOOP_SELECT_DESCRIPTION": "Döngü modunu {{mode}} olarak ayarla", - "MESSAGE_LOOP_MODE": "✅ Döngü `{{mode}}` olarak ayarlandı.", - "MESSAGE_MOVE_SUCCESS": "✅ Şarkı sırası başarıyla değiştirildi.", + "MESSAGE_LOOP_MODE": "✅ Döngü `{{mode}}` olarak ayarlandı", + "MESSAGE_MOVE_SUCCESS": "✅ Şarkı sırası başarıyla değiştirildi", "MESSAGE_MOVE_WRONG_INDEX": "❌ Lütfen geçerli bir dizin aralığı girin. (1 - {{max}})", "MESSAGE_NOW_PLAYING_BUTTOMTITLE": "\nve {{length}} diğer şarkı", "MESSAGE_NOW_PLAYING_SAVE_BUTTON": "Şarkıyı Kaydet", @@ -145,40 +145,40 @@ "MESSAGE_NOW_PLAYING_TITLE": "Şu An Çalıyor: {{title}}\n\n", "MESSAGE_NOW_PLAYING_TITLE_WITH_REQUESTER": "🎵 Şu An Çalıyor: {{title}} | {{requester}} tarafından istendi", "UNKNOWN_USER": "Bilinmiyor", - "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı.", - "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı.", - "MESSAGE_PLAY_ARGS_ERROR": "❌ Aramak istediğiniz müziğin adını yazın.", - "MESSAGE_PLAY_MUSIC_ADD": "✅ Müzik eklendi.", - "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok.", + "MESSAGE_PAUSE_FAIL": "❌ Müzik duraklatılamadı", + "MESSAGE_PAUSE_MUSIC_PAUSED": "⏸️ Müzik duraklatıldı", + "MESSAGE_PAUSE_SUCCESS": "⏸️ Müzik duraklatıldı", + "MESSAGE_PLAY_ARGS_ERROR": "❌ Parça adı veya linki girin", + "MESSAGE_PLAY_MUSIC_ADD": "✅ Parça kuyruğa eklendi", + "MESSAGE_PLAY_SEARCH_NO_MATCH": "❌ Eşleşme yok", "MESSAGE_PLAY_SELECT_DURATION": "Süre: {{label}}", "MESSAGE_PLAY_SELECT_TITLE": "Müziği seçin", - "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi.", - "MESSAGE_REMOVE_FAIL": "❌ Müzik kaldırılamadı.", - "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir şarkı seçerek **kaldırın** veya seçimi iptal etmek için başka bir şey girin. ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcuttan sonra kuyrukta müzik yok.", - "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi.", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ Müzik devam ettirildi.", - "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi.", + "MESSAGE_REMOVE_CANCEL": "✅ Kaldırma işlemi iptal edildi", + "MESSAGE_REMOVE_FAIL": "❌ Parça kaldırılamadı", + "MESSAGE_REMOVE_INSTRUCTION": "**1** ile **{{length}}** arasında bir parça numarası seçerek **kaldırın** veya iptal için başka bir numara girin. ⬇️", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ Mevcut parçadan sonra kuyruk boş", + "MESSAGE_RESUME_FAIL": "❌ Müzik devam ettirilemedi", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ Müzik devam ettirildi", + "MESSAGE_RESUME_SUCCESS": "▶️ Müzik devam ettirildi", "MESSAGE_SEEK_ARGS_ERROR": "❌ Hedef zaman için geçersiz format.\n(**`örn: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ Arama konumu bu parçanın süresinin ötesinde. (`{{duration}}`)\nSıradaki şarkıya geçiliyor.", - "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi.", - "MESSAGE_SHUFFLE_SUCCESS": "✅ Müzik karıştırıldı.", - "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı.", - "MESSAGE_SKIP_SUCCESS": "✅ Müzik atlandı.", - "MESSAGE_STOP_SUCCESS": "✅ Müzik durduruldu.", + "MESSAGE_SEEK_SKIP": "✅ Belirtilen konum (`{{duration}}`), parçanın süresinden uzun.\nSıradaki parçaya geçiyorum.", + "MESSAGE_SEEK_SUCCESS": "✅ Müzik `{{duration}}` konumuna getirildi", + "MESSAGE_SHUFFLE_SUCCESS": "✅ Çalma listesi karıştırıldı", + "MESSAGE_SKIP_FAIL": "❌ Müzik atlanamadı", + "MESSAGE_SKIP_SUCCESS": "✅ Parça atlandı", + "MESSAGE_STOP_SUCCESS": "✅ Çalma durduruldu, kuyruk temizlendi", "MESSAGE_VOLUME_ARGS_ERROR": "Mevcut ses seviyesi: **{{volume}}** 🔊\n**Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **Ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.**", - "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", - "MESSAGE_VOLUME_SAME": "❌ Değiştirmek istediğiniz ses seviyesi zaten mevcut ses seviyesiyle aynı.", + "MESSAGE_VOLUME_SELECT": "Mevcut ses seviyesi: **{{volume}}%** 🔊\n**Lütfen ayarlamak istediğiniz ses seviyesini seçin:**", + "MESSAGE_VOLUME_SAME": "❌ Bu ses seviyesi zaten ayarlı", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 Bu sunucu için varsayılan ses seviyesi **{{volume}}%** olarak değiştirildi! (En fazla: {{maxVolume}}%)", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", - "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Varsayılan ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin.", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR": "Mevcut varsayılan ses seviyesi: **{{volume}}%** 🔊\nDeğiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin", + "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ Varsayılan ses seviyesini değiştirmek için `1` ile `{{maxVolume}}` arasında bir sayı girin", "ERROR_QUEUE_LIMIT_REACHED": "❌ Kuyruk sınırınıza ulaştınız! Şu anda kuyrukta **{{current}}** şarkı var (sınır: **{{limit}}**). Daha fazla eklemek için lütfen bazı şarkıların bitmesini bekleyin.", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ Küresel kuyruk sınırına ulaşıldı! Kuyrukta şu anda **{{total}}** şarkı var (küresel sınır: **{{limit}}**). Daha fazla eklemek için lütfen bazı şarkıların bitmesini bekleyin.", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, kuyruk sınırınız nedeniyle **{{skipped}}** şarkı atlandı (**{{current}}/{{limit}}**).", - "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, küresel sıra sınırı nedeniyle **{{skipped}}** şarkı atlandı (**{{total}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, kuyruk sınırınız nedeniyle **{{skipped}}** şarkı atlandı (**{{current}}/{{limit}}**)", + "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ Çalma listesi kısmen eklendi: **{{added}}** şarkı eklendi, küresel sıra sınırı nedeniyle **{{skipped}}** şarkı atlandı (**{{total}}/{{limit}}**)", "MESSAGE_SERVER_GUILD_ID": "Sunucu Kimliği", "MESSAGE_SERVER_GUILD": "Sunucu", "MESSAGE_SERVER_MEMBERS": "Üyeler", @@ -197,73 +197,76 @@ "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", - "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste yöneticisi başlatılmadı.", + "ERROR_BLACKLIST_NOT_INITIALIZED": "Kara liste henüz yüklenmedi, sonra tekrar deneyin", "HELP_SELECT_MUSIC_PLACEHOLDER": "Bir Müzik komutu seçin", - "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Yardımcı komut seçin", + "HELP_SELECT_UTILITY_PLACEHOLDER": "Bir Hizmet komutu seçin", "HELP_COMMAND_ALIASES": "Takma Adlar: {{aliases}}", "HELP_COMMAND_NONE": "Yok", "LABEL_FILTER_CLEAR": "Filtreleri temizle", - "CONFIG_PLAYLIST_DESCRIPTION": "Çalma listelerini kaydedin, yükleyin, düzenleyin veya M3U bağlantısından içe aktarın.", + "CONFIG_PLAYLIST_DESCRIPTION": "Çalma listelerini kaydedin, yükleyin, düzenleyin veya M3U bağlantısından içe aktarın", "CONFIG_PLAYLIST_USAGE": "playlist ", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Mevcut sırayı bir çalma listesi olarak kaydeder.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Kaydedilmiş çalma listesini yükler ve oynatır.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Sunucudaki tüm çalma listelerini listeler.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Çalma listesindeki parçaları gösterir.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Çalma listesini siler.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uzak M3U dosyasından çalma listesini içe aktarır.", - "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Çalma listesinden bir parçayı kaldırır.", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_SAVE": "Mevcut sırayı bir çalma listesi olarak kaydeder", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_PLAY": "Kaydedilmiş çalma listesini yükler ve oynatır", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_LIST": "Sunucudaki tüm çalma listelerini listeler", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_INFO": "Çalma listesindeki parçaları gösterir", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_DELETE": "Çalma listesini siler", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_IMPORT": "Uzak M3U dosyasından çalma listesini içe aktarır", + "CONFIG_PLAYLIST_OPTION_SUBCOMMAND_REMOVE_TRACK": "Çalma listesinden bir parçayı kaldırır", "CONFIG_PLAYLIST_OPTION_NAME": "Çalma listesi adı", "CONFIG_PLAYLIST_OPTION_SEARCH": "Çalma listesinde aranacak parça adı (İsteğe bağlı)", "CONFIG_PLAYLIST_OPTION_URL": "M3U dosyası indirme bağlantısı", "CONFIG_PLAYLIST_OPTION_INDEX": "Kaldırılacak parça sırası (1'den başlar)", - "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Çalma listesi yöneticisi başlatılamadı.", - "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Lütfen geçerli bir alt komut girin (save, play, list, info, delete, import, remove-track).", - "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Bilinmeyen alt komut.", - "ERROR_PLAYLIST_NO_PLAYING": "❌ Şu anda hiçbir şey çalmıyor veya sıra boş.", - "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Lütfen bir çalma listesi adı belirtin.", - "ERROR_PLAYLIST_SAVE_FAILED": "❌ Çalma listesi kaydedilemedi.", - "ERROR_PLAYLIST_NOT_FOUND": "❌ `{{name}}` adlı çalma listesi bulunamadı veya boş.", - "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ `{{name}}` çalma listesinde `{{searchQuery}}` parçası bulunamadı.", + "ERROR_PLAYLIST_NOT_INITIALIZED": "❌ Çalma listesi yöneticisi başlatılamadı", + "ERROR_PLAYLIST_SUBCOMMAND_INVALID": "❌ Lütfen geçerli bir alt komut girin (save, play, list, info, delete, import, remove-track)", + "ERROR_PLAYLIST_SUBCOMMAND_UNKNOWN": "❌ Bilinmeyen alt komut", + "ERROR_PLAYLIST_NO_PLAYING": "❌ Şu anda hiçbir şey çalmıyor veya sıra boş", + "ERROR_PLAYLIST_NAME_REQUIRED": "❌ Lütfen bir çalma listesi adı belirtin", + "ERROR_PLAYLIST_SAVE_FAILED": "❌ Çalma listesi kaydedilemedi", + "ERROR_PLAYLIST_NOT_FOUND": "❌ `{{name}}` adlı çalma listesi bulunamadı veya boş", + "ERROR_PLAYLIST_TRACK_NOT_FOUND": "❌ `{{name}}` çalma listesinde `{{searchQuery}}` parçası bulunamadı", "ERROR_PLAYLIST_M3U_DOWNLOAD_FAILED": "❌ M3U dosyası indirilemedi. (HTTP durumu: {{status}})", "ERROR_PLAYLIST_M3U_DOWNLOAD_BLOCKED": "❌ M3U indirmesi güvenlik politikası tarafından engellendi. {{timeoutSeconds}} saniye içinde yanıt veren ve {{maxSizeMiB}} MiB boyutunu aşmayan herkese açık bir HTTPS URL kullanın.", - "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Bir çalma listesi en fazla {{limit}} parça içerebilir.", + "ERROR_PLAYLIST_TRACK_LIMIT": "❌ Bir çalma listesi en fazla {{limit}} parça içerebilir", "ERROR_PLAYLIST_IMPORT_FAILED": "❌ M3U çalma listesi ayrıştırılamadı ve kaydedilemedi. Formatın doğru olduğundan emin olun.", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ Parça kaldırılamadı. Geçerli bir indeks girdiğinizden emin olun.", - "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi.", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor...", - "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi. (Başarılı: {{added}}, Atlanan: {{skipped}})", - "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok.", + "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ Mevcut sıradaki {{count}} parça `{{name}}` çalma listesine kaydedildi", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 `{{name}}` çalma listesindeki {{count}} parça aranıyor ve ekleniyor..", + "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 `{{name}}` çalma listesi yüklendi: {{added}} eklendi, {{skipped}} atlandı", + "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ Bu sunucuda kaydedilmiş çalma listesi yok", "MESSAGE_PLAYLIST_LIST_TITLE": "Sunucu Çalma Listeleri", "MESSAGE_PLAYLIST_INFO_TITLE": "Çalma Listesi Bilgisi: {{name}} ({{count}} parça)", "MESSAGE_PLAYLIST_INFO_REMAINING": "\n\n*ve {{remaining}} parça daha.*", - "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ `{{name}}` çalma listesi silindi.", + "MESSAGE_PLAYLIST_DELETE_SUCCESS": "✅ `{{name}}` çalma listesi silindi", "MESSAGE_PLAYLIST_IMPORT_SUCCESS": "✅ Harici M3U çalma listesi içe aktarıldı!\n`{{name}}` çalma listesine {{count}} parça eklendi.", - "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ `{{name}}` çalma listesinden #{{index}} parça kaldırıldı.", - "CONFIG_RADIO_DESCRIPTION": "İçe aktarılan radyo çalma listesinden belirli bir türü/kanalı arayın ve oynatın.", + "MESSAGE_PLAYLIST_REMOVE_SUCCESS": "✅ `{{name}}` çalma listesinden #{{index}} parça kaldırıldı", + "CONFIG_RADIO_DESCRIPTION": "İçe aktarılan radyo çalma listesinden belirli bir türü/kanalı arayın ve oynatın", "CONFIG_RADIO_USAGE": "radio [kanal]", "CONFIG_RADIO_OPTION_PLAYLIST": "Aranacak çalma listesi adı", "CONFIG_RADIO_OPTION_CHANNEL": "Oynatılacak radyo kanalı veya türü (örn. workout, club, deep)", "ERROR_RADIO_USAGE_EXAMPLE": "❌ Kullanım: `{{prefix}}radio [çalma listesi adı] [kanal adı]` (örn. `{{prefix}}radio Test Dance`)", - "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin.", + "ERROR_RADIO_REQUIRED_ARGS": "❌ Lütfen hem çalma listesi adını hem de kanal adını girin", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ `{{playlistName}}` çalma listesinde `{{channelQuery}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", - "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\n(örn., `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ Radyo çalma listesi hazır değil.\nLütfen önce `/playlist import {{name}} ` ile radyo çalma listesini içe aktarın.\nDosya, EXTINF etiketleri içeren bir M3U formatında olmalı.", "ERROR_RADIO_NO_MATCH": "❌ `{{query}}` ile eşleşen radyo kanalı bulunamadı.\nKullanılabilir örnekler: {{samples}} vb.", "ERROR_RADIO_LOAD_FAILED": "❌ Radyo kanalı yüklenirken hata oluştu: {{error}}", - "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`).", - "MESSAGE_RADIO_SEARCHING": "🔄 **{{playlist}}** çalma listesindeki **{{title}}** radyo kanalı aranıyor...", + "ERROR_RADIO_STREAM_FAILED": "❌ Radyo yayını yüklenemedi veya bulunamadı (`{{title}}`)", + "MESSAGE_RADIO_SEARCHING": "🔄 **{{playlist}}** çalma listesindeki **{{title}}** radyo kanalı aranıyor..", "MESSAGE_RADIO_PLAYING": "📻 **{{playlist}}** çalma listesinden **{{title}}** radyo kanalı oynatılmaya başlandı!", "MESSAGE_FOOTER_PAGE": "Sayfa {{page}} / {{totalPages}}", - "ERROR_ONLY_COMMAND_USER_PAGINATE": "Sadece komutu çalıştıran kullanıcı sayfaları değiştirebilir.", - "ERROR_ONLY_COMMAND_USER_CONFIRM": "Sadece komutu çalıştıran kullanıcı bu işlemi onaylayabilir.", + "ERROR_ONLY_COMMAND_USER_PAGINATE": "Sadece komutu çalıştıran kullanıcı sayfaları değiştirebilir", + "ERROR_ONLY_COMMAND_USER_CONFIRM": "Sadece komutu çalıştıran kullanıcı bu işlemi onaylayabilir", "MESSAGE_PLAYLIST_EXISTS_TITLE": "⚠️ Çalma Listesi Zaten Var", "MESSAGE_PLAYLIST_EXISTS_DESCRIPTION": "**`{{name}}`** adında bir çalma listesi (toplam {{count}} parça) zaten mevcut.\nİçe aktarılan çalma listesiyle değiştirmek istiyor musunuz?", "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "**`{{name}}`** isimli oynatma listesi (toplam {{count}} şarkı) zaten mevcut.\nMevcut sıradaki şarkılarla değiştirmek istiyor musunuz?", "LABEL_OVERWRITE": "Üzerine Yaz", "LABEL_CANCEL": "İptal", - "MESSAGE_PLAYLIST_OVERWRITING": "Çalma listesinin üzerine yazılıyor...", - "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Çalma listesi içe aktarma iptal edildi.", - "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu.", - "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi.", - "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu.", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş." -} \ No newline at end of file + "MESSAGE_PLAYLIST_OVERWRITING": "Çalma listesinin üzerine yazılıyor..", + "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ Çalma listesi içe aktarma iptal edildi", + "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ İçe aktarma onay süresi doldu", + "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ Oynatma listesini kaydetme iptal edildi", + "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ Kayıt onay süresi doldu", + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ Mevcut parça atlandı. Sıra şimdi boş.", + "ERROR_PLAYLIST_DISABLED": "❌ Çalma listesi özellikleri şu anda devre dışı.", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ M3U çalma listeleri tamamen sıraya eklenemez. Lütfen belirli bir istasyon seçmek için `/radio` komutunu kullanın.", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ M3U çalma listesi içe aktarma yalnızca Yöneticiler ve DJ'ler ile sınırlıdır." +} diff --git a/src/locales/tr-TR/embeds.json b/src/locales/tr-TR/embeds.json index 9a4dc7d3..06d2e007 100644 --- a/src/locales/tr-TR/embeds.json +++ b/src/locales/tr-TR/embeds.json @@ -1,41 +1,41 @@ { "DASHBOARD_TITLE": "Panel", - "DASHBOARD_FINISH_PLAYING": "Kuyruk sonlandı.", + "DASHBOARD_FINISH_PLAYING": "Kuyruğun çalınması tamamlandı", "DASHBOARD_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{duration}}**\n────────────────────\nSes Yüksekliği: `{{volume}}%`\nDöngü: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**Talep Eden:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", - "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı.", + "DASHBOARD_VOICE_CHANNEL_CONNECT_SUCCESS": "Ses bağlantısı başarıyla yapıldı", "MAINTAIN_DESCRIPTION": "Bot birkaç dakika içinde bakım moduna alınacaktır. Bu süre boyunca bazı özellikler geçici olarak kullanılamayabilir.", "MAINTAIN_TITLE": "⚠️ Bakım Notu ⚠️", - "MESSAGE_BLACKLIST": "Ses kanalında kara listeye alınmış kullanıcı(lar) bulunuyor.", + "MESSAGE_BLACKLIST": "Ses kanalında kara listedeki kullanıcılar var", "MESSAGE_COMMAND": "Komut **{{command}}**", - "MESSAGE_FILTER": "Filtre modu seçiliyor **{{effectName}}**", - "NODE_ALL_ACTIVE": "Tüm bağlantı noktaları etkin", + "MESSAGE_FILTER": "Filtre **{{effectName}}** olarak ayarlandı", + "NODE_ALL_ACTIVE": "Tüm düğümler etkin", "NODE_CONNECTED": "Gecikme: **{{ping}}ms**", "NODE_DISCONNECTED": "**BAĞLANTI KESİLDİ**", - "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir bağlantı noktası seçin.", + "NODE_STATUS_ARGS_ERROR": "❌ Lütfen geçerli bir düğüm adı girin", "NODE_STATUS_CPU_TITLE": "⚙️ CPU", "NODE_STATUS_CPU_VALUE": "Çekirdekler: **{{cores}}**\nSistem yükü: **{{systemLoad}}**\nLavalink yükü: **{{lavalinkLoad}}**", "NODE_STATUS_INFO_TITLE": "🏷️ Bilgi", - "NODE_STATUS_INFO_VALUE": "Versiyon: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nBuild time: **{{buildTime}}**", + "NODE_STATUS_INFO_VALUE": "Versiyon: **{{version}}**\nJVM: **{{jvm}}**\nLavaplayer: **{{lavaplayer}}**\nGit: **{{git}}**\nDerleme süresi: **{{buildTime}}**", "NODE_STATUS_MEMORY_TITLE": "📑 Hafıza", - "NODE_STATUS_MEMORY_VALUE": "Kullanılan: **{{used}}**\nFree: **{{free}}**\nAllocated: **{{allocated}}**\nReservable: **{{reservable}}**\n", + "NODE_STATUS_MEMORY_VALUE": "Kullanılan: **{{used}}**\nBoş: **{{free}}**\nAyrılan: **{{allocated}}**\nRezerve: **{{reservable}}**\n", "NODE_STATUS_PING": "Gecikme", "NODE_STATUS_STATS_TITLE": "📊 İstatistikler", - "NODE_STATUS_STATS_VALUE": "Bağlantı Süresi: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nPlayer: **{{playerCount}}**\nPlaying players: **{{playingCount}}**", - "NODE_STATUS_TITLE": "🛰️ Bağlantı Noktası", - "NODE_STATUS_TITLE_2": "🛰️ Node '{{nodeName}}' durumu", - "NODE_UNHEALTHY": "⚠️ {{unhealthValue}} tane bağlantı noktasının bağlantısı kesildi", + "NODE_STATUS_STATS_VALUE": "Bağlantı Süresi: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} ms**\nOynatıcı: **{{playerCount}}**\nÇalan oynatıcılar: **{{playingCount}}**", + "NODE_STATUS_TITLE": "🛰️ Düğüm Durumu", + "NODE_STATUS_TITLE_2": "🛰️ Düğüm '{{nodeName}}' durumu", + "NODE_UNHEALTHY": "⚠️ {{unhealthValue}} düğümün bağlantısı kesildi", "PING_DESCRIPTION": "Bot: **{{botPing}}**\nAPI: **{{apiPing}}ms**", - "PING_TITLE": "🛰️ Gecikme'", + "PING_TITLE": "🛰️ Gecikme", "QUEUE_ADD_PLAYLIST": "Oynatma listesi eklendi", "QUEUE_ADD_TRACK": "Parça eklendi", "QUEUE_LIST_LOOP_MODE": "Döngü: {{repeatMode}}", - "QUEUE_LIST_TITLE": "Kuyruktaki Liste", + "QUEUE_LIST_TITLE": "Kuyruk Listesi", "REMOVE_LIST_LOOP_MODE": "Döngü: {{repeatMode}}", - "REMOVE_LIST_TITLE": "Liste kaldırıldı.", - "REMOVE_TRACK_TITLE": "Parça kaldırıldı.", - "SERVER_TITLE": "**{{name}}** bulunan sunucular", + "REMOVE_LIST_TITLE": "Kaldırma Listesi", + "REMOVE_TRACK_TITLE": "Kaldırılan Parça", + "SERVER_TITLE": "**{{name}}** olan sunucular", "SERVER_FIELD_DJ_ROLE": "DJ Rolü", "SERVER_FIELD_ADMINS": "Yöneticiler", "SERVER_FIELD_DJ_USERS": "DJ Kullanıcıları", @@ -49,4 +49,4 @@ "QUEUE_NOW_PLAYING": "**Şu An Çalıyor:**", "QUEUE_EMPTY": "*Kuyrukta şarkı yok*", "QUEUE_HEADER": "**Kuyruk:**" -} \ No newline at end of file +} diff --git a/src/locales/tr-TR/events.json b/src/locales/tr-TR/events.json index ed921011..daf7f0c1 100644 --- a/src/locales/tr-TR/events.json +++ b/src/locales/tr-TR/events.json @@ -1,25 +1,25 @@ { - "ERROR_GET_GUILD_DATA_CACHE": "❌ ⚠ Sunucu verileri önbelleğe alınamadı!", - "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz.", - "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın.", - "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz.", - "ERROR_NOT_PLAYING": "❌ Şuan çalan bir parça bulunmuyor.", - "ERROR_REQUIRE_ADMIN": "❌ Bu komut yönetici izinleri gerektirmektedir.", - "ERROR_REQUIRE_DJ": "❌ Bu komut DJ izinleri gerektirmektedir.", - "ERROR_SEND_PRIVATE_MESSAGE": "❌ Size özel mesaj gönderemiyorum.", - "ERROR_SLASH_NOT_ENABLE": "❌ Slash ile çalışan komutlar aktif değil.", - "ERROR_TIME_EXPIRED": "❌ Süre doldu.", - "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot ayrıldı.", - "MESSAGE_BOT_STOP": "✅ Bot durduruldu.", - "MESSAGE_MUSIC_SHUFFLE": "✅ Kuyruk karıştırıldı.", + "ERROR_GET_GUILD_DATA_CACHE": "❌ Sunucu verileri önbellekten alınamadı", + "ERROR_NOT_IN_SAME_VOICE_CHANNEL": "❌ Bot ile aynı ses kanalında bulunmuyorsunuz", + "ERRPR_NOT_IN_SPECIFIC_VOICE_CHANNEL": "Lütfen belirtilen ses kanalına <#{{channelId}}> katılın ve beni çağırın", + "ERROR_NOT_IN_VOICE_CHANNEL": "❌ Herhangi bir ses kanalına bağlı değilsiniz", + "ERROR_NOT_PLAYING": "❌ Şuan çalan bir parça bulunmuyor", + "ERROR_REQUIRE_ADMIN": "❌ Bu komut yönetici izinleri gerektirmektedir", + "ERROR_REQUIRE_DJ": "❌ Bu komut DJ izinleri gerektirmektedir", + "ERROR_SEND_PRIVATE_MESSAGE": "❌ Size özel mesaj gönderemiyorum", + "ERROR_SLASH_NOT_ENABLE": "❌ Slash ile çalışan komutlar aktif değil", + "ERROR_TIME_EXPIRED": "❌ Süre doldu", + "MESSAGE_BOT_LEAVE_CHANNEL": "✅ Bot ayrıldı", + "MESSAGE_BOT_STOP": "✅ Bot durduruldu", + "MESSAGE_MUSIC_SHUFFLE": "✅ Kuyruk karıştırıldı", "MESSAGE_NOW_PLAYING_SUBTITLE": "Sanatçı: **{{author}}**\nSüre: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "Şuan Çalıyor: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- Sayfa {{curPage}}/{{maxPage}} -----", - "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz.", - "LOOP_MODE_OFF": "Off", - "LOOP_MODE_SINGLE": "Single", - "LOOP_MODE_ALL": "All", - "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim.", - "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}`", + "MESSAGE_SELECT_LOOP_MODE": "Döngü modunu seçiniz", + "LOOP_MODE_OFF": "Kapalı", + "LOOP_MODE_SINGLE": "Tek", + "LOOP_MODE_ALL": "Tümü", + "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ Parçanın adını size özel mesaj olarak gönderdim", + "MESSAGE_SET_LOOP_MODE": "✅ Döngü modu `{{mode}}` olarak ayarlandı", "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ Bu kanalda komut kullanamazsınız! Lütfen <#{{channelId}}> metin kanalını kullanın." -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/commands.json b/src/locales/zh-TW/commands.json index c82fb8d1..7f5f2e2c 100644 --- a/src/locales/zh-TW/commands.json +++ b/src/locales/zh-TW/commands.json @@ -9,12 +9,12 @@ "MESSAGE_BLACKLIST_ADDED": "✅ <@{{userId}}> 已被加入黑名單。", "MESSAGE_BLACKLIST_REMOVED": "✅ <@{{userId}}> 已從黑名單中移除。", "MESSAGE_BLACKLIST_ALREADY_LISTED": "❌ <@{{userId}}> 已在黑名單中。", - "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> 不在動態黑名單中。", - "MESSAGE_BLACKLIST_LIST_TITLE": "動態黑名單", - "MESSAGE_BLACKLIST_LIST_EMPTY": "動態黑名單為空。", + "MESSAGE_BLACKLIST_NOT_LISTED": "❌ <@{{userId}}> 不在黑名單中。", + "MESSAGE_BLACKLIST_LIST_TITLE": "黑名單", + "MESSAGE_BLACKLIST_LIST_EMPTY": "黑名單目前是空的。", "CONFIG_CLEAR_DESCRIPTION": "清除隊列中的所有音樂", "CONFIG_CLEAR_USAGE": "clear", - "CONFIG_DASHBOARD_DESCRIPTION": "將儀表板更新到底部", + "CONFIG_DASHBOARD_DESCRIPTION": "將儀表板移至底部", "CONFIG_DASHBOARD_USAGE": "dashboard", "CONFIG_DJ_DESCRIPTION": "將使用者添加為 DJ 或顯示 DJ 列表", "CONFIG_DJ_USAGE": "dj [@使用者]", @@ -37,8 +37,8 @@ "CONFIG_LOOP_OPTION_DESCRIPTION": "音樂循環模式", "CONFIG_MOVE_DESCRIPTION": "交換隊列中兩首歌曲的位置", "CONFIG_MOVE_USAGE": "move <位置1> <位置2>", - "CONFIG_MOVE_OPTION_DESCRIPTION": "交換歌曲位置1", - "CONFIG_MOVE_OPTION_DESCRIPTION_2": "交換歌曲位置2", + "CONFIG_MOVE_OPTION_DESCRIPTION": "歌曲位置 1", + "CONFIG_MOVE_OPTION_DESCRIPTION_2": "歌曲位置 2", "CONFIG_NODE_DESCRIPTION": "顯示節點的運作狀態", "CONFIG_NODE_USAGE": "node [節點名稱]", "CONFIG_NODE_OPTION_DESCRIPTION": "節點名稱", @@ -62,9 +62,9 @@ "CONFIG_QUEUE_DESCRIPTION": "顯示當前播放列表", "CONFIG_QUEUE_USAGE": "queue", "CONFIG_REMOVE_DESCRIPTION": "從播放列表中選擇要移除的歌曲", - "CONFIG_REMOVE_USAGE": "remove <曲目索引號> [從索引到索引2的曲目]", - "CONFIG_REMOVE_OPTION_DESCRIPTION": "曲目索引號", - "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "從索引到索引2的曲目", + "CONFIG_REMOVE_USAGE": "remove <曲目編號> [起始到結束]", + "CONFIG_REMOVE_OPTION_DESCRIPTION": "曲目編號", + "CONFIG_REMOVE_OPTION_DESCRIPTION_2": "曲目範圍", "CONFIG_RESUME_DESCRIPTION": "恢復播放已暫停的曲目", "CONFIG_RESUME_USAGE": "resume", "CONFIG_SEARCH_DESCRIPTION": "輸入歌曲名稱以搜尋", @@ -75,7 +75,7 @@ "CONFIG_SEEK_OPTION_DESCRIPTION": "目標時間 (例如: 3m20s, 1:20:55)", "CONFIG_SERVER_DESCRIPTION": "顯示當前活躍的伺服器", "CONFIG_SERVER_USAGE": "server", - "CONFIG_SHUFFLE_DESCRIPTION": "隨機播放列表", + "CONFIG_SHUFFLE_DESCRIPTION": "隨機打亂播放清單", "CONFIG_SHUFFLE_USAGE": "shuffle", "CONFIG_SKIP_DESCRIPTION": "跳過當前曲目", "CONFIG_SKIP_USAGE": "skip", @@ -98,7 +98,7 @@ "ERROR_PLAY_JOIN_CHANNEL": "❌ 我無法加入語音頻道。", "ERROR_PLAY_MUSIC": "❌ 服務遇到一些問題,請稍後再試。({{reason}})", "ERROR_PLAY_SEARCH": "❌ 找不到結果。({{reason}})", - "ERROR_TIME_EXPIRED": "❌ 時間已過期。", + "ERROR_TIME_EXPIRED": "❌ 等待時間已逾時。", "MESSAGE_CLEAR_SUCCESS": "✅ 播放清單已清空。", "MESSAGE_DASHBOARD_SUCCESS": "✅ 控制面板已更新。", "MESSAGE_DJ_NO_PERMISSION": "您需要 DJ 或管理員權限才能添加 DJ。", @@ -156,20 +156,20 @@ "MESSAGE_REMOVE_CANCEL": "✅ 已取消移除。", "MESSAGE_REMOVE_FAIL": "❌ 音樂移除失敗。", "MESSAGE_REMOVE_INSTRUCTION": "從 **1** 到 **{{length}}** 中選擇一首歌曲來 **移除**,或輸入其他內容以取消選擇。 ⬇️", - "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 當前播放之後沒有音樂。", + "MESSAGE_REMOVE_QUEUE_EMPTY": "❌ 目前曲目之後的佇列是空的。", "MESSAGE_RESUME_FAIL": "❌ 音樂恢復失敗。", - "MESSAGE_RESUME_MUSIC_RESUMED": "⏸️ 音樂已恢復播放。", + "MESSAGE_RESUME_MUSIC_RESUMED": "▶️ 音樂已恢復播放。", "MESSAGE_RESUME_SUCCESS": "▶️ 音樂已恢復播放。", "MESSAGE_SEEK_ARGS_ERROR": "❌ 目標時間格式無效。\n(**`例如: 3m20s, 1m 50s, 1:20:55, 5:20`**)", - "MESSAGE_SEEK_SKIP": "✅ 搜尋位置超出了此曲目的時長。(`{{duration}}`)\n正在跳到下一首歌曲。", + "MESSAGE_SEEK_SKIP": "✅ 指定的位置 (`{{duration}}`) 超出此曲目的時長。\n正在跳到下一首歌曲。", "MESSAGE_SEEK_SUCCESS": "✅ 音樂已跳轉至 `{{duration}}`。", - "MESSAGE_SHUFFLE_SUCCESS": "✅ 音樂已隨機播放。", + "MESSAGE_SHUFFLE_SUCCESS": "✅ 播放清單已打亂。", "MESSAGE_SKIP_FAIL": "❌ 音樂跳過失敗。", "MESSAGE_SKIP_SUCCESS": "✅ 音樂已跳過。", - "MESSAGE_STOP_SUCCESS": "✅ 音樂已停止。", + "MESSAGE_STOP_SUCCESS": "✅ 已停止播放並清空佇列。", "MESSAGE_VOLUME_ARGS_ERROR": "當前音量:**{{volume}}** 🔊\n**要更改音量,請輸入 `1` 到 `{{maxVolume}}` 之間的數字。**", "MESSAGE_VOLUME_ARGS_ERROR_2": "❌ **輸入 `1` 到 `{{maxVolume}}` 之間的數字以更改音量。**", - "MESSAGE_VOLUME_SELECT": "當前音量:**{{volume}}%** 🔊\\n**請選擇您要設置的音量:**", + "MESSAGE_VOLUME_SELECT": "當前音量:**{{volume}}%** 🔊\n**請選擇您要設置的音量:**", "MESSAGE_VOLUME_SAME": "❌ 您要更改的音量已經是當前音量。", "MESSAGE_VOLUME_SUCCESS": "🔊 **{{volume}}**/**{{maxVolume}}**%", "MESSAGE_VOLUME_DEFAULT_SUCCESS": "🔊 此伺服器的預設音量已更改為 **{{volume}}%**! (最大值: {{maxVolume}}%)", @@ -177,7 +177,7 @@ "MESSAGE_VOLUME_DEFAULT_ARGS_ERROR_2": "❌ 輸入 `1` 到 `{{maxVolume}}` 之間的數字以更改預設音量。", "ERROR_QUEUE_LIMIT_REACHED": "❌ 您已達到隊列限制!您目前在隊列中有 **{{current}}** 首歌曲(限制:**{{limit}}**)。請等待一些歌曲播放完畢後再添加。", "ERROR_GLOBAL_QUEUE_LIMIT_REACHED": "❌ 已達到全域隊列限制!隊列目前有 **{{total}}** 首歌曲(全域限制:**{{limit}}**)。請等待一些歌曲播放完畢後再添加。", - "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於您的隊列限制跳過 **{{skipped}}** 首歌曲 (**{{current}}/{{limit}}**).", + "MESSAGE_PLAYLIST_PARTIAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於您的隊列限制跳過 **{{skipped}}** 首歌曲 (**{{current}}/{{limit}}**)", "MESSAGE_PLAYLIST_PARTIAL_GLOBAL": "⚠️ 播放清單部分已添加:已添加 **{{added}}** 首歌曲,由於全域隊列限制,跳過 **{{skipped}}** 首歌曲 (**{{total}}/{{limit}}**)。", "MESSAGE_SERVER_GUILD_ID": "伺服器 ID", "MESSAGE_SERVER_GUILD": "伺服器", @@ -192,14 +192,14 @@ "UNIT_HOURS": "小時", "UNIT_MINUTES": "分", "UNIT_SECONDS": "秒", - "UNIT_BYTES": "Bytes", + "UNIT_BYTES": "位元組", "UNIT_KB": "KB", "UNIT_MB": "MB", "UNIT_GB": "GB", "UNIT_TB": "TB", "ERROR_BLACKLIST_NOT_INITIALIZED": "黑名單管理器尚未初始化。", "HELP_SELECT_MUSIC_PLACEHOLDER": "選擇音樂指令", - "HELP_SELECT_UTILITY_PLACEHOLDER": "選擇工具指令", + "HELP_SELECT_UTILITY_PLACEHOLDER": "選擇服務指令", "HELP_COMMAND_ALIASES": "別名: {{aliases}}", "HELP_COMMAND_NONE": "無", "LABEL_FILTER_CLEAR": "清除效果", @@ -230,7 +230,7 @@ "ERROR_PLAYLIST_IMPORT_FAILED": "❌ 解析並儲存 M3U 播放清單失敗。請確認格式是否正確。", "ERROR_PLAYLIST_REMOVE_FAILED": "❌ 刪除歌曲失敗。請確認編號是否正確。", "MESSAGE_PLAYLIST_SAVE_SUCCESS": "✅ 已將目前佇列中的 {{count}} 首歌曲儲存至播放清單 `{{name}}`。", - "MESSAGE_PLAYLIST_LOAD_START": "🔄 正在從播放清單 `{{name}}` 搜尋並新增 {{count}} 首歌曲...", + "MESSAGE_PLAYLIST_LOAD_START": "🔄 正在從播放清單 `{{name}}` 搜尋並新增 {{count}} 首歌曲..", "MESSAGE_PLAYLIST_LOAD_COMPLETE": "🎵 播放清單 `{{name}}` 載入完成。 (成功: {{added}}首, 略過/失敗: {{skipped}}首)", "MESSAGE_PLAYLIST_LIST_EMPTY": "ℹ️ 目前伺服器沒有已儲存的播放清單。", "MESSAGE_PLAYLIST_LIST_TITLE": "伺服器播放清單列表", @@ -246,11 +246,11 @@ "ERROR_RADIO_USAGE_EXAMPLE": "❌ 正確用法: `{{prefix}}radio [播放清單名稱] [電台名稱]` (例如: `{{prefix}}radio Test Dance`)", "ERROR_RADIO_REQUIRED_ARGS": "❌ 請同時輸入播放清單名稱與電台頻道名稱。", "ERROR_RADIO_NO_MATCH_PLAYLIST": "❌ 在播放清單 `{{playlistName}}` 中找不到與 `{{channelQuery}}` 符合的電台頻道。\n可用範例: {{samples}} 等", - "ERROR_RADIO_NOT_READY": "❌ 電台播放清單尚未就緒。\n請先使用 `/playlist import {{name}} ` 指令匯入電台網址清單。\n(例如: `https://gist.githubusercontent.com/Dum4G/aa456d7d6578144a45fdfd3a65f019ef/raw/`)", + "ERROR_RADIO_NOT_READY": "❌ 電台播放清單尚未就緒。\n請先使用 `/playlist import {{name}} ` 指令匯入電台網址清單。\n檔案必須是包含 EXTINF 標籤的 M3U 格式。", "ERROR_RADIO_NO_MATCH": "❌ 找不到與 `{{query}}` 符合的電台頻道。\n可用範例: {{samples}} 等", "ERROR_RADIO_LOAD_FAILED": "❌ 載入電台頻道時發生錯誤: {{error}}", "ERROR_RADIO_STREAM_FAILED": "❌ 無法載入或找不到電台串流 (`{{title}}`)。", - "MESSAGE_RADIO_SEARCHING": "🔄 正在搜尋播放清單 **{{playlist}}** 中的電台頻道 **{{title}}**...", + "MESSAGE_RADIO_SEARCHING": "🔄 正在搜尋播放清單 **{{playlist}}** 中的電台頻道 **{{title}}**..", "MESSAGE_RADIO_PLAYING": "📻 已開始播放來自播放清單 **{{playlist}}** 的電台頻道 **{{title}}**!", "MESSAGE_FOOTER_PAGE": "第 {{page}} / {{totalPages}} 頁", "ERROR_ONLY_COMMAND_USER_PAGINATE": "只有執行該指令的使用者才能切換頁面。", @@ -260,10 +260,13 @@ "MESSAGE_PLAYLIST_SAVE_EXISTS_DESCRIPTION": "名為 **`{{name}}`** 的播放清單(共 {{count}} 首歌曲)已經存在。\n您是否要使用當前佇列覆蓋它?", "LABEL_OVERWRITE": "覆蓋", "LABEL_CANCEL": "取消", - "MESSAGE_PLAYLIST_OVERWRITING": "正在覆蓋播放清單...", + "MESSAGE_PLAYLIST_OVERWRITING": "正在覆蓋播放清單..", "MESSAGE_PLAYLIST_IMPORT_CANCELLED": "❌ 已取消匯入播放清單。", "MESSAGE_PLAYLIST_IMPORT_TIMEOUT": "⏱️ 匯入確認逾時。", "MESSAGE_PLAYLIST_SAVE_CANCELLED": "❌ 已取消儲存播放清單。", "MESSAGE_PLAYLIST_SAVE_TIMEOUT": "⏱️ 儲存確認逾時。", - "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。" -} \ No newline at end of file + "MESSAGE_SKIP_EMPTY_DISCONNECT": "⏭️ 已略過目前歌曲。佇列現已清空。", + "ERROR_PLAYLIST_DISABLED": "❌ 目前已停用播放清單功能。", + "ERROR_PLAYLIST_M3U_CANNOT_PLAY_ALL": "❌ 無法將 M3U 播放清單整本加入佇列。請使用 `/radio` 指令選擇特定電台頻道。", + "ERROR_PLAYLIST_IMPORT_ADMIN_DJ_ONLY": "❌ 匯入 M3U 播放清單僅限管理員及 DJ 權限使用者使用。" +} diff --git a/src/locales/zh-TW/embeds.json b/src/locales/zh-TW/embeds.json index 60f295d2..74dc8489 100644 --- a/src/locales/zh-TW/embeds.json +++ b/src/locales/zh-TW/embeds.json @@ -1,6 +1,6 @@ { "DASHBOARD_TITLE": "控制面板", - "DASHBOARD_FINISH_PLAYING": "音樂播放已結束", + "DASHBOARD_FINISH_PLAYING": "播放清單播放已結束", "DASHBOARD_SUBTITLE": "作者: **{{author}}**\n時長: **{{duration}}**\n────────────────────\n音量: `{{volume}}%`\n循環模式: `{{repeatMode}}`", "DASHBOARD_REQUESTER_INFO": "\n────────────────────\n**請求者:** <@{{requesterId}}>", "DASHBOARD_DJ_INFO": "\n**DJ:** {{djDisplay}}", @@ -9,7 +9,7 @@ "MAINTAIN_TITLE": "⚠️ 維護通知 ⚠️", "MESSAGE_BLACKLIST": "語音頻道中有被列入黑名單的用戶。", "MESSAGE_COMMAND": "指令 **{{command}}**", - "MESSAGE_FILTER": "將效果模式設置為 **{{effectName}}**", + "MESSAGE_FILTER": "效果已設置為:**{{effectName}}**", "NODE_ALL_ACTIVE": "所有節點均已啟用", "NODE_CONNECTED": "延遲: **{{ping}}ms**", "NODE_DISCONNECTED": "**已斷線**", @@ -22,7 +22,7 @@ "NODE_STATUS_MEMORY_VALUE": "已使用: **{{used}}**\n可用: **{{free}}**\n分配: **{{allocated}}**\n可保留: **{{reservable}}**\n", "NODE_STATUS_PING": "延遲", "NODE_STATUS_STATS_TITLE": "📊 統計", - "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n正在播放的播放器: **{{playingCount}}**", + "NODE_STATUS_STATS_VALUE": "運行時間: **{{uptime}}**\n**{{pingKey}}**: **{{nodePing}} 毫秒**\n播放器: **{{playerCount}}**\n播放中: **{{playingCount}}**", "NODE_STATUS_TITLE": "🛰️ 節點狀態", "NODE_STATUS_TITLE_2": "🛰️ 節點 '{{nodeName}}' 狀態", "NODE_UNHEALTHY": "⚠️ 有 {{unhealthValue}} 個節點已斷線", @@ -42,11 +42,11 @@ "STATUS_DESCRIPTION": "**• 服務於 {{serverCount}} 個伺服器**\n**• 正在 {{playingCount}} 個伺服器上播放**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "STATUS_LATENCY_TITLE": "🛰️ 延遲", "STATUS_LATENCY_VALUE": "機器人: **{{botPing}}**\nAPI: **{{apiPing}} 毫秒**", - "STATUS_SYSTEM_TITLE": "⚙️ SYSTEM", + "STATUS_SYSTEM_TITLE": "⚙️ 系統", "STATUS_SYSTEM_VALUE": "作業系統: **{{os_version}}**\nNode.js: **{{node_version}}**\nDiscord.js: **{{dc_version}}**\nLavaShark: **{{shark_version}}**\nCPU: **{{cpu}}**\n運行時間: **{{uptime}}**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "STATUS_USAGE_TITLE": "📊 資源使用", "STATUS_USAGE_VALUE": "CPU: **{{cpuUsage}}**\nRam: **{{ramUsage}}**\nHeap: **{{heapUsage}}**\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "QUEUE_NOW_PLAYING": "**正在播放:**", "QUEUE_EMPTY": "*播放清單中沒有歌曲*", "QUEUE_HEADER": "**播放清單:**" -} \ No newline at end of file +} diff --git a/src/locales/zh-TW/events.json b/src/locales/zh-TW/events.json index 79a6ca21..a9179a00 100644 --- a/src/locales/zh-TW/events.json +++ b/src/locales/zh-TW/events.json @@ -11,15 +11,15 @@ "ERROR_TIME_EXPIRED": "❌ 時間已過期。", "MESSAGE_BOT_LEAVE_CHANNEL": "✅ 機器人已離開頻道。", "MESSAGE_BOT_STOP": "✅ 機器人已停止。", - "MESSAGE_MUSIC_SHUFFLE": "✅ 音樂已隨機播放。", + "MESSAGE_MUSIC_SHUFFLE": "✅ 播放清單已打亂。", "MESSAGE_NOW_PLAYING_SUBTITLE": "作者: **{{author}}**\n時長: **{{label}}**\n", "MESSAGE_NOW_PLAYING_TITLE": "正在播放: {{title}}\n\n", "MESSAGE_QUEUE_PAGE": "\n\n----- 第 {{curPage}}/{{maxPage}} 頁 -----", "MESSAGE_SELECT_LOOP_MODE": "選擇歌曲循環模式。", - "LOOP_MODE_OFF": "Off", - "LOOP_MODE_SINGLE": "Single", - "LOOP_MODE_ALL": "All", + "LOOP_MODE_OFF": "關閉", + "LOOP_MODE_SINGLE": "單曲", + "LOOP_MODE_ALL": "全部", "MESSAGE_SEND_PRIVATE_MESSAGE": "✅ 我已將歌曲名稱發送至您的私人訊息中。", - "MESSAGE_SET_LOOP_MODE": "✅ 循環模式設置為 `{{mode}}`", - "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。\n" -} \ No newline at end of file + "MESSAGE_SET_LOOP_MODE": "✅ 已選擇循環模式:`{{mode}}`", + "MESSAGE_SPECIFIC_CHANNEL_WARN": "❌ 您無法在此頻道中執行指令。請使用 <#{{channelId}}> 文字頻道。" +} diff --git a/src/utils/functions/cleanSearchQuery.ts b/src/utils/functions/cleanSearchQuery.ts new file mode 100644 index 00000000..16376b3b --- /dev/null +++ b/src/utils/functions/cleanSearchQuery.ts @@ -0,0 +1,15 @@ +/** + * Clean and normalize search query for Lavashark / Lavalink + * Handles Discord link wrappers like ``, trims whitespace, + * and extracts clean URLs or search terms. + */ +export function cleanSearchQuery(query: string): string { + if (!query) return ''; + + let cleaned = query.trim(); + + // Remove Discord link wrapper brackets: -> https://... + cleaned = cleaned.replace(/^<(.+)>$/, '$1').trim(); + + return cleaned; +} diff --git a/src/utils/functions/generateRandomKey.ts b/src/utils/functions/generateRandomKey.ts new file mode 100644 index 00000000..12b577da --- /dev/null +++ b/src/utils/functions/generateRandomKey.ts @@ -0,0 +1,5 @@ +import { randomBytes } from 'crypto'; + +export const generateRandomKey = (): string => { + return randomBytes(32).toString('hex'); +}; diff --git a/src/utils/functions/lavasharkRequest.ts b/src/utils/functions/lavasharkRequest.ts new file mode 100644 index 00000000..e5fef773 --- /dev/null +++ b/src/utils/functions/lavasharkRequest.ts @@ -0,0 +1,79 @@ +import type { LavaShark } from 'lavashark'; + + +const DEFAULT_RETRY_ATTEMPTS = 3; +const DEFAULT_RETRY_DELAY_MS = 2000; + +/** + * Wait for the given number of milliseconds + */ +export const sleep = (ms: number): Promise => + new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Search a track with retries. + * Lavalink nodes enforce REST rate limits (HTTP 429); a single retry after a + * short delay lets transient rate limiting resolve without skipping tracks. + */ +export async function searchWithRetry( + lavashark: LavaShark, + query: string, + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.search(query); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} + +/** + * Decode a track from its encoded string with retries + */ +export async function decodeTrackWithRetry( + lavashark: LavaShark, + encoded: string, + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.decodeTrack(encoded); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} + +/** + * Decode many tracks in a single request. + * Lavalink rate limits REST calls (HTTP 429) aggressively; bulk-decoding a + * whole saved queue with one `/decodetracks` request avoids exhausting the + * node's request budget entirely. + */ +export async function decodeTracksWithRetry( + lavashark: LavaShark, + encodedTracks: string[], + attempts: number = DEFAULT_RETRY_ATTEMPTS, + retryDelayMs: number = DEFAULT_RETRY_DELAY_MS, +): Promise { + for (let attempt = 0; attempt < attempts; attempt++) { + try { + return await lavashark.decodeTracks(encodedTracks); + } catch { + if (attempt < attempts - 1) { + await sleep(retryDelayMs); + } + } + } + return null; +} diff --git a/src/utils/languageUtils.ts b/src/utils/languageUtils.ts index 4caaf54c..174849a4 100644 --- a/src/utils/languageUtils.ts +++ b/src/utils/languageUtils.ts @@ -2,7 +2,7 @@ const LANGUAGE_NAMES: Record = { 'en-US': 'English (en-US)', 'ko-KR': '한국어 (ko-KR)', 'ru-RU': 'Русский (ru-RU)', - 'sr-RS': 'Српски (sr-RS)', + 'sr-RS': 'Srpski (sr-RS)', 'tr-TR': 'Türkçe (tr-TR)', 'zh-TW': '繁體中文 (zh-TW)', 'ar': 'العربية (ar)',