yukimu 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/BitrateOptimizer.d.ts +52 -0
  2. package/dist/BitrateOptimizer.d.ts.map +1 -0
  3. package/dist/BitrateOptimizer.js +115 -0
  4. package/dist/BitrateOptimizer.js.map +1 -0
  5. package/dist/ConnectionPool.d.ts +52 -3
  6. package/dist/ConnectionPool.d.ts.map +1 -1
  7. package/dist/ConnectionPool.js +124 -32
  8. package/dist/ConnectionPool.js.map +1 -1
  9. package/dist/Constants.d.ts +27 -2
  10. package/dist/Constants.d.ts.map +1 -1
  11. package/dist/Constants.js +133 -11
  12. package/dist/Constants.js.map +1 -1
  13. package/dist/Logger.d.ts +22 -0
  14. package/dist/Logger.d.ts.map +1 -0
  15. package/dist/Logger.js +74 -0
  16. package/dist/Logger.js.map +1 -0
  17. package/dist/Node.d.ts +17 -4
  18. package/dist/Node.d.ts.map +1 -1
  19. package/dist/Node.js +226 -88
  20. package/dist/Node.js.map +1 -1
  21. package/dist/Player.d.ts +74 -7
  22. package/dist/Player.d.ts.map +1 -1
  23. package/dist/Player.js +317 -92
  24. package/dist/Player.js.map +1 -1
  25. package/dist/Plugin.d.ts +19 -1
  26. package/dist/Plugin.d.ts.map +1 -1
  27. package/dist/Plugin.js +8 -0
  28. package/dist/Plugin.js.map +1 -1
  29. package/dist/Queue.d.ts +69 -2
  30. package/dist/Queue.d.ts.map +1 -1
  31. package/dist/Queue.js +138 -17
  32. package/dist/Queue.js.map +1 -1
  33. package/dist/Resolver.d.ts +33 -2
  34. package/dist/Resolver.d.ts.map +1 -1
  35. package/dist/Resolver.js +225 -33
  36. package/dist/Resolver.js.map +1 -1
  37. package/dist/Rest.d.ts +27 -4
  38. package/dist/Rest.d.ts.map +1 -1
  39. package/dist/Rest.js +157 -25
  40. package/dist/Rest.js.map +1 -1
  41. package/dist/TrackCache.d.ts +30 -5
  42. package/dist/TrackCache.d.ts.map +1 -1
  43. package/dist/TrackCache.js +119 -15
  44. package/dist/TrackCache.js.map +1 -1
  45. package/dist/WsQueue.d.ts +31 -1
  46. package/dist/WsQueue.d.ts.map +1 -1
  47. package/dist/WsQueue.js +70 -12
  48. package/dist/WsQueue.js.map +1 -1
  49. package/dist/Yukimu.d.ts +38 -5
  50. package/dist/Yukimu.d.ts.map +1 -1
  51. package/dist/Yukimu.js +150 -57
  52. package/dist/Yukimu.js.map +1 -1
  53. package/dist/connector/Connector.d.ts +26 -0
  54. package/dist/connector/Connector.d.ts.map +1 -1
  55. package/dist/connector/Connector.js +28 -0
  56. package/dist/connector/Connector.js.map +1 -1
  57. package/dist/connector/DiscordJS.d.ts +20 -1
  58. package/dist/connector/DiscordJS.d.ts.map +1 -1
  59. package/dist/connector/DiscordJS.js +44 -2
  60. package/dist/connector/DiscordJS.js.map +1 -1
  61. package/dist/connector/Eris.d.ts +12 -1
  62. package/dist/connector/Eris.d.ts.map +1 -1
  63. package/dist/connector/Eris.js +40 -6
  64. package/dist/connector/Eris.js.map +1 -1
  65. package/dist/connector/Oceanic.d.ts +12 -1
  66. package/dist/connector/Oceanic.d.ts.map +1 -1
  67. package/dist/connector/Oceanic.js +39 -4
  68. package/dist/connector/Oceanic.js.map +1 -1
  69. package/dist/errors/YukimuError.d.ts +40 -4
  70. package/dist/errors/YukimuError.d.ts.map +1 -1
  71. package/dist/errors/YukimuError.js +79 -8
  72. package/dist/errors/YukimuError.js.map +1 -1
  73. package/dist/index.d.ts +13 -4
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +30 -16
  76. package/dist/index.js.map +1 -1
  77. package/dist/plugins/AutoResume.d.ts +18 -1
  78. package/dist/plugins/AutoResume.d.ts.map +1 -1
  79. package/dist/plugins/AutoResume.js +109 -21
  80. package/dist/plugins/AutoResume.js.map +1 -1
  81. package/dist/plugins/AutoplayPlugin.d.ts +35 -0
  82. package/dist/plugins/AutoplayPlugin.d.ts.map +1 -0
  83. package/dist/plugins/AutoplayPlugin.js +111 -0
  84. package/dist/plugins/AutoplayPlugin.js.map +1 -0
  85. package/dist/plugins/InactivityPlugin.d.ts +30 -0
  86. package/dist/plugins/InactivityPlugin.d.ts.map +1 -0
  87. package/dist/plugins/InactivityPlugin.js +86 -0
  88. package/dist/plugins/InactivityPlugin.js.map +1 -0
  89. package/dist/plugins/PlayerMoved.d.ts +23 -1
  90. package/dist/plugins/PlayerMoved.d.ts.map +1 -1
  91. package/dist/plugins/PlayerMoved.js +57 -12
  92. package/dist/plugins/PlayerMoved.js.map +1 -1
  93. package/dist/types.d.ts +198 -71
  94. package/dist/types.d.ts.map +1 -1
  95. package/dist/types.js +1 -0
  96. package/dist/types.js.map +1 -1
  97. package/package.json +49 -5
  98. package/.cache/replit/env/latest +0 -88
  99. package/.cache/replit/env/latest.json +0 -1
  100. package/.cache/replit/modules/nodejs-20.res +0 -1
  101. package/.cache/replit/modules/python-3.11.res +0 -1
  102. package/.cache/replit/modules/replit.res +0 -1
  103. package/.cache/replit/modules.stamp +0 -0
  104. package/.cache/replit/nix/dotreplitenv.json +0 -1
  105. package/.cache/replit/toolchain.json +0 -1
  106. package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.packager.installForAll.0 +0 -9
  107. package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.shell.exec.1 +0 -1
  108. package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.packager.installForAll.0 +0 -1
  109. package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.shell.exec.1 +0 -1
  110. package/.local/state/workflow-logs/U0AinJQVHonnwGjj0RXLn/configure_your_app.packager.installForAll.0 +0 -2
  111. package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.packager.installForAll.0 +0 -1
  112. package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.shell.exec.1 +0 -1
  113. package/.replit +0 -7
  114. package/.upm/store.json +0 -1
  115. package/src/ConnectionPool.ts +0 -114
  116. package/src/Constants.ts +0 -45
  117. package/src/Node.ts +0 -302
  118. package/src/Player.ts +0 -332
  119. package/src/Plugin.ts +0 -7
  120. package/src/Queue.ts +0 -66
  121. package/src/Resolver.ts +0 -90
  122. package/src/Rest.ts +0 -127
  123. package/src/TrackCache.ts +0 -46
  124. package/src/WsQueue.ts +0 -58
  125. package/src/Yukimu.ts +0 -213
  126. package/src/connector/Connector.ts +0 -13
  127. package/src/connector/DiscordJS.ts +0 -26
  128. package/src/connector/Eris.ts +0 -24
  129. package/src/connector/Oceanic.ts +0 -22
  130. package/src/errors/YukimuError.ts +0 -31
  131. package/src/index.ts +0 -24
  132. package/src/plugins/AutoResume.ts +0 -37
  133. package/src/plugins/PlayerMoved.ts +0 -26
  134. package/src/types.ts +0 -145
  135. package/tsconfig.json +0 -22
package/dist/Player.js CHANGED
@@ -1,24 +1,47 @@
1
1
  "use strict";
2
+ /** Yukimu v2.0.0 — Player with State Safety & Bitrate Optimization */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.Player = void 0;
4
5
  const Queue_1 = require("./Queue");
6
+ const BitrateOptimizer_1 = require("./BitrateOptimizer");
5
7
  const Constants_1 = require("./Constants");
6
8
  const YukimuError_1 = require("./errors/YukimuError");
9
+ /**
10
+ * Represents an audio player for a specific Discord guild.
11
+ *
12
+ * Critical fixes from v1:
13
+ * 1. play() — only sets playing=true AFTER the REST call succeeds
14
+ * 2. pause() — paused tracks are still "playing" from Lavalink's perspective
15
+ * 3. moveToNode() — properly re-sends voice state to the new node
16
+ * 4. sendVoicePayload() — deferred until connector is ready (safe for startup)
17
+ * 5. Proper cleanup in destroy() — all state, timers, and references
18
+ * 6. Input validation on filter values
19
+ * 7. Boost-tier-aware audio optimization
20
+ */
7
21
  class Player {
22
+ manager;
23
+ node;
24
+ queue;
25
+ guildId;
26
+ voiceChannelId;
27
+ textChannelId;
28
+ state = Constants_1.PlayerState.DISCONNECTED;
29
+ playing = false;
30
+ paused = false;
31
+ position = 0;
32
+ ping = -1;
33
+ lastUpdated = Date.now();
34
+ volume;
35
+ loop = "none";
36
+ autoplay = false;
37
+ sessionId = null;
38
+ voiceToken = null;
39
+ voiceEndpoint = null;
40
+ selfDeaf;
41
+ selfMute;
42
+ filters = {};
43
+ data = new Map();
8
44
  constructor(manager, node, options) {
9
- this.state = Constants_1.PlayerState.DISCONNECTED;
10
- this.playing = false;
11
- this.paused = false;
12
- this.position = 0;
13
- this.ping = -1;
14
- this.lastUpdated = Date.now();
15
- this.loop = "none";
16
- this.autoplay = false;
17
- this.sessionId = null;
18
- this.voiceToken = null;
19
- this.voiceEndpoint = null;
20
- this.filters = {};
21
- this.data = new Map();
22
45
  this.manager = manager;
23
46
  this.node = node;
24
47
  this.queue = new Queue_1.Queue();
@@ -28,21 +51,46 @@ class Player {
28
51
  this.volume = options.volume ?? 100;
29
52
  this.selfDeaf = options.selfDeaf ?? true;
30
53
  this.selfMute = options.selfMute ?? false;
54
+ // Send the voice state update to Discord (join the channel)
55
+ // This is safe — connector.sendPayload handles missing guilds gracefully
31
56
  this.sendVoicePayload(options.voiceChannelId, this.selfDeaf, this.selfMute);
32
57
  this.state = Constants_1.PlayerState.CONNECTING;
33
58
  }
34
- get connected() { return this.state === Constants_1.PlayerState.CONNECTED; }
59
+ // ─── Getters ────────────────────────────────────────────────────────────
60
+ get connected() {
61
+ return this.state === Constants_1.PlayerState.CONNECTED;
62
+ }
63
+ /**
64
+ * Estimated real-time position accounting for time since last update.
65
+ * Returns the actual position (capped at track length).
66
+ */
35
67
  get realPosition() {
36
68
  if (!this.playing || this.paused)
37
69
  return this.position;
38
- return Math.min(this.position + (Date.now() - this.lastUpdated), this.queue.current?.info?.length ?? this.position);
70
+ const trackLength = this.queue.current?.info?.length ?? Infinity;
71
+ return Math.min(this.position + (Date.now() - this.lastUpdated), trackLength);
39
72
  }
73
+ /**
74
+ * Get the current boost tier of the guild this player is in.
75
+ */
76
+ get boostTier() {
77
+ return this.manager.connector.getGuildBoostTier(this.guildId);
78
+ }
79
+ // ─── Voice ──────────────────────────────────────────────────────────────
40
80
  sendVoicePayload(channelId, selfDeaf, selfMute) {
41
81
  this.manager.connector.sendPayload(this.guildId, {
42
82
  op: 4,
43
- d: { guild_id: this.guildId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute },
83
+ d: {
84
+ guild_id: this.guildId,
85
+ channel_id: channelId,
86
+ self_deaf: selfDeaf,
87
+ self_mute: selfMute,
88
+ },
44
89
  });
45
90
  }
91
+ /**
92
+ * Check if we have all voice connection data and send it to Lavalink.
93
+ */
46
94
  checkVoiceReady() {
47
95
  if (!this.sessionId || !this.voiceToken || !this.voiceEndpoint)
48
96
  return;
@@ -53,22 +101,38 @@ class Player {
53
101
  channelId: this.voiceChannelId,
54
102
  };
55
103
  if (this.node.version === 4) {
56
- this.node.rest.updatePlayer(this.guildId, { voice: voiceState }).catch(console.error);
104
+ this.node.rest.updatePlayer(this.guildId, { voice: voiceState }).catch((err) => {
105
+ this.manager.logger.error(`Failed to send voice update for guild ${this.guildId}: ${err}`);
106
+ });
57
107
  }
58
108
  else {
59
109
  this.node.send({
60
110
  op: "voiceUpdate",
61
111
  guildId: this.guildId,
62
112
  sessionId: this.sessionId,
63
- event: { token: this.voiceToken, guild_id: this.guildId, endpoint: this.voiceEndpoint },
113
+ event: {
114
+ token: this.voiceToken,
115
+ guild_id: this.guildId,
116
+ endpoint: this.voiceEndpoint,
117
+ },
118
+ }).catch((err) => {
119
+ this.manager.logger.error(`Failed to send voice update for guild ${this.guildId}: ${err}`);
64
120
  });
65
121
  }
66
122
  this.state = Constants_1.PlayerState.CONNECTED;
67
123
  }
124
+ // ─── Playback ───────────────────────────────────────────────────────────
125
+ /**
126
+ * Play a track. If no track is provided, plays the current queue track.
127
+ *
128
+ * FIX: Only sets `playing = true` after the REST call succeeds.
129
+ * In v1, state was set before the REST call, causing ghost playing state on failure.
130
+ */
68
131
  async play(track, options) {
69
132
  const toPlay = track ?? this.queue.current;
70
- if (!toPlay)
71
- throw new YukimuError_1.PlayerError("No track to play");
133
+ if (!toPlay) {
134
+ throw new YukimuError_1.PlayerError("No track to play", YukimuError_1.ErrorCode.PLAYER_NO_TRACK);
135
+ }
72
136
  this.queue.current = toPlay;
73
137
  if (this.node.version === 4) {
74
138
  await this.node.rest.updatePlayer(this.guildId, {
@@ -79,7 +143,7 @@ class Player {
79
143
  }, options?.noReplace ?? false);
80
144
  }
81
145
  else {
82
- this.node.send({
146
+ await this.node.send({
83
147
  op: "play",
84
148
  guildId: this.guildId,
85
149
  track: toPlay.encoded,
@@ -88,27 +152,36 @@ class Player {
88
152
  ...(options?.endTime !== undefined && { endTime: String(options.endTime) }),
89
153
  });
90
154
  }
155
+ // Only update state AFTER the call succeeds
91
156
  this.playing = true;
92
157
  this.paused = false;
93
158
  this.lastUpdated = Date.now();
94
159
  }
160
+ /**
161
+ * Pause or unpause playback.
162
+ *
163
+ * FIX: `playing` now reflects "has an active track" independently of pause state.
164
+ * A paused player is still "playing" (it has an active track, just paused).
165
+ */
95
166
  async pause(state = true) {
96
167
  if (this.node.version === 4) {
97
168
  await this.node.rest.updatePlayer(this.guildId, { paused: state });
98
169
  }
99
170
  else {
100
- this.node.send({ op: "pause", guildId: this.guildId, pause: state });
171
+ await this.node.send({ op: "pause", guildId: this.guildId, pause: state });
101
172
  }
102
173
  this.paused = state;
103
- this.playing = !state;
174
+ // `playing` stays true — the track is still loaded, just paused
175
+ }
176
+ async resume() {
177
+ return this.pause(false);
104
178
  }
105
- async resume() { return this.pause(false); }
106
179
  async stop() {
107
180
  if (this.node.version === 4) {
108
181
  await this.node.rest.updatePlayer(this.guildId, { track: { encoded: null } });
109
182
  }
110
183
  else {
111
- this.node.send({ op: "stop", guildId: this.guildId });
184
+ await this.node.send({ op: "stop", guildId: this.guildId });
112
185
  }
113
186
  this.playing = false;
114
187
  this.paused = false;
@@ -126,41 +199,77 @@ class Player {
126
199
  }
127
200
  return next;
128
201
  }
202
+ /**
203
+ * Play the previous track (from history).
204
+ * @returns The previous track, or null if no history exists
205
+ */
206
+ async previous() {
207
+ const prev = this.queue.previous[0];
208
+ if (!prev)
209
+ return null;
210
+ // Move current back to front of queue
211
+ if (this.queue.current) {
212
+ this.queue.add(this.queue.current, 0);
213
+ }
214
+ // Remove from previous history
215
+ this.queue.previous.shift();
216
+ await this.play(prev);
217
+ return prev;
218
+ }
219
+ /**
220
+ * Restart the currently playing track from the beginning.
221
+ */
222
+ async replay() {
223
+ if (!this.queue.current) {
224
+ throw new YukimuError_1.PlayerError("No track to replay", YukimuError_1.ErrorCode.PLAYER_NO_TRACK);
225
+ }
226
+ await this.play(this.queue.current, { startTime: 0 });
227
+ }
129
228
  async seek(position) {
130
- if (!this.queue.current?.info?.isSeekable)
131
- throw new YukimuError_1.PlayerError("Current track is not seekable");
229
+ if (!this.queue.current?.info?.isSeekable) {
230
+ throw new YukimuError_1.PlayerError("Current track is not seekable", YukimuError_1.ErrorCode.PLAYER_NOT_SEEKABLE);
231
+ }
232
+ if (position < 0)
233
+ position = 0;
132
234
  if (this.node.version === 4) {
133
235
  await this.node.rest.updatePlayer(this.guildId, { position });
134
236
  }
135
237
  else {
136
- this.node.send({ op: "seek", guildId: this.guildId, position });
238
+ await this.node.send({ op: "seek", guildId: this.guildId, position });
137
239
  }
138
240
  this.position = position;
139
241
  this.lastUpdated = Date.now();
140
242
  }
243
+ // ─── Volume ─────────────────────────────────────────────────────────────
141
244
  async setVolume(volume) {
142
- if (volume < 0 || volume > 1000)
143
- throw new YukimuError_1.PlayerError("Volume must be between 0 and 1000");
245
+ if (typeof volume !== "number" || isNaN(volume)) {
246
+ throw new YukimuError_1.PlayerError("Volume must be a number", YukimuError_1.ErrorCode.PLAYER_INVALID_VOLUME);
247
+ }
248
+ volume = Math.floor(Math.max(0, Math.min(1000, volume)));
144
249
  if (this.node.version === 4) {
145
250
  await this.node.rest.updatePlayer(this.guildId, { volume });
146
251
  }
147
252
  else {
148
- this.node.send({ op: "volume", guildId: this.guildId, volume });
253
+ await this.node.send({ op: "volume", guildId: this.guildId, volume });
149
254
  }
150
255
  this.volume = volume;
151
256
  }
257
+ // ─── Filters ────────────────────────────────────────────────────────────
152
258
  async setFilters(filters) {
153
259
  this.filters = { ...this.filters, ...filters };
154
260
  if (this.node.version === 4) {
155
261
  await this.node.rest.updatePlayer(this.guildId, { filters: this.filters });
156
262
  }
157
263
  else {
158
- if (filters.equalizer)
159
- this.node.send({ op: "equalizer", guildId: this.guildId, bands: filters.equalizer });
264
+ // v3: equalizer is sent separately
265
+ if (filters.equalizer) {
266
+ await this.node.send({ op: "equalizer", guildId: this.guildId, bands: filters.equalizer });
267
+ }
160
268
  const rest = { ...filters };
161
269
  delete rest.equalizer;
162
- if (Object.keys(rest).length > 0)
163
- this.node.send({ op: "filters", guildId: this.guildId, ...rest });
270
+ if (Object.keys(rest).length > 0) {
271
+ await this.node.send({ op: "filters", guildId: this.guildId, ...rest });
272
+ }
164
273
  }
165
274
  }
166
275
  async clearFilters() {
@@ -169,128 +278,221 @@ class Player {
169
278
  await this.node.rest.updatePlayer(this.guildId, { filters: {} });
170
279
  }
171
280
  else {
172
- this.node.send({ op: "filters", guildId: this.guildId });
281
+ await this.node.send({ op: "filters", guildId: this.guildId });
173
282
  }
174
283
  }
284
+ // ─── Filter Presets ─────────────────────────────────────────────────────
175
285
  async setBassBoost(level) {
176
- const presets = {
177
- off: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
178
- low: [0.2, 0.15, 0.1, 0.05, 0, -0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0],
179
- medium: [0.4, 0.3, 0.2, 0.1, 0.05, -0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0],
180
- high: [0.6, 0.5, 0.4, 0.25, 0.1, -0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
181
- extreme: [1.0, 0.8, 0.6, 0.4, 0.2, -0.2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
182
- };
183
- await this.setFilters({ equalizer: presets[level].map((gain, band) => ({ band, gain })) });
286
+ const tier = this.boostTier;
287
+ if (level === "off") {
288
+ await this.setFilters({ equalizer: BitrateOptimizer_1.BitrateOptimizer.getFlatEQ() });
289
+ return;
290
+ }
291
+ // Scale bass based on boost tier to prevent clipping
292
+ const scales = { low: 0.3, medium: 0.5, high: 0.75, extreme: 1.0 };
293
+ const scale = scales[level] ?? 0.5;
294
+ const tierScale = tier >= 2 ? 1.0 : tier === 1 ? 0.8 : 0.6;
295
+ const effectiveScale = scale * tierScale;
296
+ const eq = [
297
+ { band: 0, gain: 0.6 * effectiveScale },
298
+ { band: 1, gain: 0.5 * effectiveScale },
299
+ { band: 2, gain: 0.4 * effectiveScale },
300
+ { band: 3, gain: 0.25 * effectiveScale },
301
+ { band: 4, gain: 0.1 * effectiveScale },
302
+ { band: 5, gain: -0.05 * effectiveScale },
303
+ ...Array.from({ length: 9 }, (_, i) => ({ band: i + 6, gain: 0 })),
304
+ ];
305
+ await this.setFilters({ equalizer: eq });
184
306
  }
185
307
  async setNightcore(enabled) {
186
- await this.setFilters({ timescale: enabled ? { speed: 1.2, pitch: 1.2, rate: 1.0 } : {} });
308
+ await this.setFilters({
309
+ timescale: enabled ? { speed: 1.2, pitch: 1.2, rate: 1.0 } : {},
310
+ });
187
311
  }
188
312
  async setVaporwave(enabled) {
189
- await this.setFilters({ timescale: enabled ? { speed: 0.8, pitch: 0.8, rate: 1.0 } : {} });
313
+ await this.setFilters({
314
+ timescale: enabled ? { speed: 0.8, pitch: 0.8, rate: 1.0 } : {},
315
+ });
190
316
  }
191
317
  async set8D(enabled) {
192
- await this.setFilters({ rotation: enabled ? { rotationHz: 0.2 } : {} });
318
+ await this.setFilters({
319
+ rotation: enabled ? { rotationHz: 0.2 } : {},
320
+ });
193
321
  }
194
322
  async setKaraoke(enabled) {
195
- await this.setFilters({ karaoke: enabled ? { level: 1.0, monoLevel: 1.0, filterBand: 220.0, filterWidth: 100.0 } : {} });
323
+ await this.setFilters({
324
+ karaoke: enabled
325
+ ? { level: 1.0, monoLevel: 1.0, filterBand: 220.0, filterWidth: 100.0 }
326
+ : {},
327
+ });
196
328
  }
197
329
  async setTremolo(enabled, frequency = 2.0, depth = 0.5) {
330
+ if (enabled) {
331
+ if (frequency <= 0 || frequency > 14) {
332
+ throw new YukimuError_1.PlayerError("Tremolo frequency must be between 0 and 14", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
333
+ }
334
+ if (depth <= 0 || depth > 1) {
335
+ throw new YukimuError_1.PlayerError("Tremolo depth must be between 0 and 1", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
336
+ }
337
+ }
198
338
  await this.setFilters({ tremolo: enabled ? { frequency, depth } : {} });
199
339
  }
200
340
  async setVibrato(enabled, frequency = 2.0, depth = 0.5) {
341
+ if (enabled) {
342
+ if (frequency <= 0 || frequency > 14) {
343
+ throw new YukimuError_1.PlayerError("Vibrato frequency must be between 0 and 14", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
344
+ }
345
+ if (depth <= 0 || depth > 1) {
346
+ throw new YukimuError_1.PlayerError("Vibrato depth must be between 0 and 1", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
347
+ }
348
+ }
201
349
  await this.setFilters({ vibrato: enabled ? { frequency, depth } : {} });
202
350
  }
203
351
  async setLowPass(enabled, smoothing = 20.0) {
352
+ if (enabled && smoothing <= 0) {
353
+ throw new YukimuError_1.PlayerError("LowPass smoothing must be greater than 0", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
354
+ }
204
355
  await this.setFilters({ lowPass: enabled ? { smoothing } : {} });
205
356
  }
357
+ /** Set playback speed (1.0 = normal, 2.0 = double speed) */
358
+ async setSpeed(speed) {
359
+ if (speed <= 0 || speed > 10) {
360
+ throw new YukimuError_1.PlayerError("Speed must be between 0 and 10", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
361
+ }
362
+ const existing = (this.filters.timescale ?? {});
363
+ await this.setFilters({
364
+ timescale: { ...existing, speed },
365
+ });
366
+ }
367
+ /** Set playback pitch (1.0 = normal) */
368
+ async setPitch(pitch) {
369
+ if (pitch <= 0 || pitch > 10) {
370
+ throw new YukimuError_1.PlayerError("Pitch must be between 0 and 10", YukimuError_1.ErrorCode.PLAYER_INVALID_FILTER);
371
+ }
372
+ const existing = (this.filters.timescale ?? {});
373
+ await this.setFilters({
374
+ timescale: { ...existing, pitch },
375
+ });
376
+ }
377
+ // ─── Queue Shortcuts ───────────────────────────────────────────────────
378
+ /**
379
+ * Add track(s) to the queue. If nothing is currently playing, plays immediately.
380
+ */
206
381
  async add(tracks, requester) {
207
- const arr = Array.isArray(tracks) ? tracks : [tracks];
208
- if (requester)
209
- arr.forEach(t => { t.requester = requester; });
382
+ const arr = Array.isArray(tracks) ? [...tracks] : [tracks];
383
+ if (requester) {
384
+ for (const t of arr) {
385
+ t["requester"] = requester;
386
+ }
387
+ }
210
388
  if (!this.queue.current && arr.length > 0) {
211
389
  const first = arr.shift();
212
- this.queue.add(arr);
390
+ if (arr.length > 0)
391
+ this.queue.add(arr);
213
392
  await this.play(first);
214
393
  }
215
394
  else {
216
395
  this.queue.add(arr);
217
396
  }
218
397
  }
219
- setLoop(mode) { this.loop = mode; }
220
- setAutoplay(enabled) { this.autoplay = enabled; }
398
+ setLoop(mode) {
399
+ this.loop = mode;
400
+ }
401
+ setAutoplay(enabled) {
402
+ this.autoplay = enabled;
403
+ }
404
+ // ─── Node Movement ─────────────────────────────────────────────────────
405
+ /**
406
+ * Move this player to a different Lavalink node.
407
+ *
408
+ * FIX: Now properly re-sends the voice state to the new node.
409
+ * In v1, the new node didn't know about the voice connection.
410
+ */
221
411
  async moveToNode(node) {
222
412
  if (this.node === node)
223
413
  return;
224
414
  const oldNode = this.node;
225
415
  this.node = node;
416
+ // Clean up on old node
226
417
  await oldNode.rest.destroyPlayer(this.guildId).catch(() => { });
418
+ // Re-send voice state to new node
419
+ this.checkVoiceReady();
420
+ // Resume playback on new node
227
421
  if (this.queue.current) {
228
422
  await this.play(this.queue.current, { startTime: this.realPosition });
229
423
  }
230
- if (Object.keys(this.filters).length > 0)
424
+ // Re-apply filters and volume
425
+ if (Object.keys(this.filters).length > 0) {
231
426
  await this.setFilters(this.filters).catch(() => { });
232
- if (this.volume !== 100)
427
+ }
428
+ if (this.volume !== 100) {
233
429
  await this.setVolume(this.volume).catch(() => { });
430
+ }
234
431
  }
432
+ /** Move the bot to a different voice channel */
235
433
  async move(channelId) {
236
434
  this.voiceChannelId = channelId;
237
435
  this.sendVoicePayload(channelId, this.selfDeaf, this.selfMute);
238
436
  }
239
- async destroy() {
240
- this.state = Constants_1.PlayerState.DISCONNECTING;
241
- this.sendVoicePayload(null, false, false);
242
- await this.node.rest.destroyPlayer(this.guildId).catch(() => { });
243
- this.playing = false;
244
- this.paused = false;
245
- this.queue.clear();
246
- this.data.clear();
247
- this.state = Constants_1.PlayerState.DISCONNECTED;
437
+ // ─── Audio Quality ──────────────────────────────────────────────────────
438
+ /**
439
+ * Apply optimal audio settings based on the guild's boost tier.
440
+ * Automatically adjusts EQ and filters for best clarity at the available bitrate.
441
+ */
442
+ async applyOptimalQuality() {
443
+ const tier = this.boostTier;
444
+ const filters = BitrateOptimizer_1.BitrateOptimizer.getOptimalFilters(tier);
445
+ await this.setFilters(filters);
446
+ this.manager.logger.debug(`Applied optimal quality for guild ${this.guildId} (boost tier ${tier}, max ${BitrateOptimizer_1.BitrateOptimizer.getMaxBitrate(tier)}kbps)`);
248
447
  }
249
- // ─── Audio Quality Presets ────────────────────────────────────────
250
448
  /** Maximum audio quality — crystal clear, enhanced bass & treble */
251
449
  async setHighQuality() {
450
+ const tier = this.boostTier;
451
+ const scale = tier >= 2 ? 1.0 : tier === 1 ? 0.8 : 0.6;
252
452
  await this.setFilters({
253
453
  equalizer: [
254
- { band: 0, gain: 0.25 }, // Sub bass boost
255
- { band: 1, gain: 0.20 }, // Bass boost
256
- { band: 2, gain: 0.15 }, // Low mid bass
257
- { band: 3, gain: 0.05 }, // Mid bass
258
- { band: 4, gain: 0.0 }, // Low mid flat
259
- { band: 5, gain: 0.0 }, // Mid flat
260
- { band: 6, gain: 0.0 }, // Upper mid flat
261
- { band: 7, gain: 0.05 }, // Presence slight boost
262
- { band: 8, gain: 0.10 }, // Upper presence
263
- { band: 9, gain: 0.15 }, // Treble boost
264
- { band: 10, gain: 0.15 }, // Upper treble
265
- { band: 11, gain: 0.10 }, // Air
266
- { band: 12, gain: 0.05 }, // Brilliance
267
- { band: 13, gain: 0.0 }, // Ultra high
268
- { band: 14, gain: 0.0 }, // Ultra high
454
+ { band: 0, gain: 0.25 * scale },
455
+ { band: 1, gain: 0.20 * scale },
456
+ { band: 2, gain: 0.15 * scale },
457
+ { band: 3, gain: 0.05 * scale },
458
+ { band: 4, gain: 0.0 },
459
+ { band: 5, gain: 0.0 },
460
+ { band: 6, gain: 0.0 },
461
+ { band: 7, gain: 0.05 * scale },
462
+ { band: 8, gain: 0.10 * scale },
463
+ { band: 9, gain: 0.15 * scale },
464
+ { band: 10, gain: 0.15 * scale },
465
+ { band: 11, gain: 0.10 * scale },
466
+ { band: 12, gain: 0.05 * scale },
467
+ { band: 13, gain: 0.0 },
468
+ { band: 14, gain: 0.0 },
269
469
  ],
270
- lowPass: { smoothing: 5.0 }, // Very light low pass for clarity
470
+ lowPass: tier >= 2 ? { smoothing: 5.0 } : {},
271
471
  });
272
472
  }
273
473
  /** Studio quality — flat, neutral, most accurate */
274
474
  async setStudioQuality() {
275
475
  await this.clearFilters();
276
476
  await this.setFilters({
277
- equalizer: Array.from({ length: 15 }, (_, i) => ({ band: i, gain: 0 })),
477
+ equalizer: BitrateOptimizer_1.BitrateOptimizer.getFlatEQ(),
278
478
  });
279
479
  }
280
- /** Concert hall effect */
480
+ /** Concert hall effect — scaled for boost tier */
281
481
  async setConcertHall() {
482
+ const tier = this.boostTier;
483
+ const scale = tier >= 2 ? 1.0 : 0.6;
282
484
  await this.setFilters({
283
485
  equalizer: [
284
- { band: 0, gain: 0.1 },
285
- { band: 1, gain: 0.1 },
286
- { band: 2, gain: 0.05 },
486
+ { band: 0, gain: 0.1 * scale },
487
+ { band: 1, gain: 0.1 * scale },
488
+ { band: 2, gain: 0.05 * scale },
287
489
  { band: 3, gain: 0.0 },
288
- { band: 4, gain: -0.05 },
490
+ { band: 4, gain: -0.05 * scale },
289
491
  { band: 5, gain: 0.0 },
290
- { band: 6, gain: 0.05 },
291
- { band: 7, gain: 0.1 },
292
- { band: 8, gain: 0.1 },
293
- { band: 9, gain: 0.05 },
492
+ { band: 6, gain: 0.05 * scale },
493
+ { band: 7, gain: 0.1 * scale },
494
+ { band: 8, gain: 0.1 * scale },
495
+ { band: 9, gain: 0.05 * scale },
294
496
  { band: 10, gain: 0.0 },
295
497
  { band: 11, gain: 0.0 },
296
498
  { band: 12, gain: 0.0 },
@@ -300,6 +502,29 @@ class Player {
300
502
  rotation: { rotationHz: 0.05 },
301
503
  });
302
504
  }
505
+ // ─── Lifecycle ──────────────────────────────────────────────────────────
506
+ /**
507
+ * Destroy this player and clean up all resources.
508
+ */
509
+ async destroy() {
510
+ this.state = Constants_1.PlayerState.DISCONNECTING;
511
+ // Leave voice channel
512
+ this.sendVoicePayload(null, false, false);
513
+ // Destroy on Lavalink
514
+ await this.node.rest.destroyPlayer(this.guildId).catch(() => { });
515
+ // Clean up all state
516
+ this.playing = false;
517
+ this.paused = false;
518
+ this.position = 0;
519
+ this.queue.clear();
520
+ this.data.clear();
521
+ this.filters = {};
522
+ this.sessionId = null;
523
+ this.voiceToken = null;
524
+ this.voiceEndpoint = null;
525
+ this.voiceChannelId = null;
526
+ this.state = Constants_1.PlayerState.DISCONNECTED;
527
+ }
303
528
  }
304
529
  exports.Player = Player;
305
530
  //# sourceMappingURL=Player.js.map