yukimu 1.0.0 → 1.2.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 (114) hide show
  1. package/.cache/replit/env/latest +88 -0
  2. package/.cache/replit/env/latest.json +1 -0
  3. package/.cache/replit/modules/python-3.11.res +1 -0
  4. package/.cache/replit/nix/dotreplitenv.json +1 -1
  5. package/.cache/replit/toolchain.json +1 -1
  6. package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.packager.installForAll.0 +1 -0
  7. package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.shell.exec.1 +1 -0
  8. package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.packager.installForAll.0 +1 -0
  9. package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.shell.exec.1 +1 -0
  10. package/.replit +4 -1
  11. package/.upm/store.json +1 -1
  12. package/dist/ConnectionPool.d.ts +34 -0
  13. package/dist/ConnectionPool.d.ts.map +1 -0
  14. package/dist/ConnectionPool.js +124 -0
  15. package/dist/ConnectionPool.js.map +1 -0
  16. package/dist/Constants.d.ts +66 -0
  17. package/dist/Constants.d.ts.map +1 -0
  18. package/dist/Constants.js +101 -0
  19. package/dist/Constants.js.map +1 -0
  20. package/dist/Node.d.ts +26 -10
  21. package/dist/Node.d.ts.map +1 -1
  22. package/dist/Node.js +203 -81
  23. package/dist/Node.js.map +1 -1
  24. package/dist/Player.d.ts +30 -29
  25. package/dist/Player.d.ts.map +1 -1
  26. package/dist/Player.js +178 -97
  27. package/dist/Player.js.map +1 -1
  28. package/dist/Plugin.d.ts +21 -0
  29. package/dist/Plugin.d.ts.map +1 -0
  30. package/dist/Plugin.js +19 -0
  31. package/dist/Plugin.js.map +1 -0
  32. package/dist/Queue.d.ts +10 -16
  33. package/dist/Queue.d.ts.map +1 -1
  34. package/dist/Queue.js +39 -32
  35. package/dist/Queue.js.map +1 -1
  36. package/dist/Resolver.d.ts +4 -21
  37. package/dist/Resolver.d.ts.map +1 -1
  38. package/dist/Resolver.js +28 -66
  39. package/dist/Resolver.js.map +1 -1
  40. package/dist/Rest.d.ts +24 -0
  41. package/dist/Rest.d.ts.map +1 -0
  42. package/dist/Rest.js +104 -0
  43. package/dist/Rest.js.map +1 -0
  44. package/dist/TrackCache.d.ts +21 -0
  45. package/dist/TrackCache.d.ts.map +1 -0
  46. package/dist/TrackCache.js +57 -0
  47. package/dist/TrackCache.js.map +1 -0
  48. package/dist/WsQueue.d.ts +20 -0
  49. package/dist/WsQueue.d.ts.map +1 -0
  50. package/dist/WsQueue.js +74 -0
  51. package/dist/WsQueue.js.map +1 -0
  52. package/dist/Yukimu.d.ts +33 -32
  53. package/dist/Yukimu.d.ts.map +1 -1
  54. package/dist/Yukimu.js +133 -65
  55. package/dist/Yukimu.js.map +1 -1
  56. package/dist/connector/Connector.d.ts +8 -0
  57. package/dist/connector/Connector.d.ts.map +1 -0
  58. package/dist/connector/Connector.js +11 -0
  59. package/dist/connector/Connector.js.map +1 -0
  60. package/dist/connector/DiscordJS.d.ts +25 -0
  61. package/dist/connector/DiscordJS.d.ts.map +1 -0
  62. package/dist/connector/DiscordJS.js +27 -0
  63. package/dist/connector/DiscordJS.js.map +1 -0
  64. package/dist/connector/Eris.d.ts +23 -0
  65. package/dist/connector/Eris.d.ts.map +1 -0
  66. package/dist/connector/Eris.js +30 -0
  67. package/dist/connector/Eris.js.map +1 -0
  68. package/dist/connector/Oceanic.d.ts +20 -0
  69. package/dist/connector/Oceanic.d.ts.map +1 -0
  70. package/dist/connector/Oceanic.js +27 -0
  71. package/dist/connector/Oceanic.js.map +1 -0
  72. package/dist/errors/YukimuError.d.ts +15 -0
  73. package/dist/errors/YukimuError.d.ts.map +1 -0
  74. package/dist/errors/YukimuError.js +35 -0
  75. package/dist/errors/YukimuError.js.map +1 -0
  76. package/dist/index.d.ts +14 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +34 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/plugins/AutoResume.d.ts +21 -0
  81. package/dist/plugins/AutoResume.d.ts.map +1 -0
  82. package/dist/plugins/AutoResume.js +52 -0
  83. package/dist/plugins/AutoResume.js.map +1 -0
  84. package/dist/plugins/PlayerMoved.d.ts +7 -0
  85. package/dist/plugins/PlayerMoved.d.ts.map +1 -0
  86. package/dist/plugins/PlayerMoved.js +30 -0
  87. package/dist/plugins/PlayerMoved.js.map +1 -0
  88. package/dist/types.d.ts +92 -38
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +0 -12
  91. package/dist/types.js.map +1 -1
  92. package/package.json +1 -1
  93. package/src/ConnectionPool.ts +131 -0
  94. package/src/Constants.ts +101 -0
  95. package/src/Node.ts +157 -174
  96. package/src/Player.ts +200 -108
  97. package/src/Plugin.ts +23 -0
  98. package/src/Queue.ts +43 -34
  99. package/src/Resolver.ts +29 -77
  100. package/src/Rest.ts +121 -0
  101. package/src/TrackCache.ts +69 -0
  102. package/src/WsQueue.ts +78 -0
  103. package/src/Yukimu.ts +156 -85
  104. package/src/connector/Connector.ts +13 -0
  105. package/src/connector/DiscordJS.ts +33 -0
  106. package/src/connector/Eris.ts +35 -0
  107. package/src/connector/Oceanic.ts +32 -0
  108. package/src/errors/YukimuError.ts +33 -0
  109. package/src/index.ts +41 -1
  110. package/src/plugins/AutoResume.ts +61 -0
  111. package/src/plugins/PlayerMoved.ts +26 -0
  112. package/src/types.ts +50 -83
  113. package/tsconfig.json +4 -2
  114. package/README.md +0 -152
package/src/Player.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { Queue } from "./Queue";
2
- import { Track, PlayerOptions, VoiceState } from "./types";
2
+ import { PlayerState } from "./Constants";
3
+ import { PlayerError } from "./errors/YukimuError";
3
4
  import type { Yukimu } from "./Yukimu";
4
5
  import type { Node } from "./Node";
6
+ import type { Track, PlayerOptions, VoiceState, FilterOptions } from "./types";
5
7
 
6
8
  export class Player {
7
9
  public readonly manager: Yukimu;
8
- public readonly node: Node;
10
+ public node: Node;
9
11
  public readonly queue: Queue;
10
12
 
11
13
  // IDs
@@ -14,23 +16,30 @@ export class Player {
14
16
  public textChannelId?: string;
15
17
 
16
18
  // State
19
+ public state: PlayerState = PlayerState.DISCONNECTED;
17
20
  public playing: boolean = false;
18
21
  public paused: boolean = false;
19
- public connected: boolean = false;
20
22
  public position: number = 0;
21
23
  public ping: number = -1;
24
+ public lastUpdated: number = Date.now();
22
25
  public volume: number;
26
+ public loop: "none" | "track" | "queue" = "none";
23
27
 
24
- // Voice connection
28
+ // Voice
25
29
  public sessionId: string | null = null;
26
30
  public voiceToken: string | null = null;
27
31
  public voiceEndpoint: string | null = null;
32
+ public selfDeaf: boolean;
33
+ public selfMute: boolean;
28
34
 
29
35
  // Filters
30
- public filters: Record<string, unknown> = {};
36
+ public filters: FilterOptions = {};
31
37
 
32
- // Loop mode
33
- public loop: "none" | "track" | "queue" = "none";
38
+ // Data store (like Kazagumo's player.data)
39
+ public readonly data: Map<string, unknown> = new Map();
40
+
41
+ // Autoplay
42
+ public autoplay: boolean = false;
34
43
 
35
44
  constructor(manager: Yukimu, node: Node, options: PlayerOptions) {
36
45
  this.manager = manager;
@@ -41,26 +50,34 @@ export class Player {
41
50
  this.voiceChannelId = options.voiceChannelId;
42
51
  this.textChannelId = options.textChannelId;
43
52
  this.volume = options.volume ?? 100;
53
+ this.selfDeaf = options.selfDeaf ?? true;
54
+ this.selfMute = options.selfMute ?? false;
55
+
56
+ this.sendVoicePayload(options.voiceChannelId, this.selfDeaf, this.selfMute);
57
+ this.state = PlayerState.CONNECTING;
58
+ }
44
59
 
45
- // Send voice channel join payload to Discord
46
- this.sendVoicePayload(options.voiceChannelId, options.selfDeaf ?? true, options.selfMute ?? false);
60
+ // ─── Getters ──────────────────────────────────────────────────────
61
+
62
+ get connected(): boolean {
63
+ return this.state === PlayerState.CONNECTED;
64
+ }
65
+
66
+ /** Real-time position estimate */
67
+ get realPosition(): number {
68
+ if (!this.playing || this.paused) return this.position;
69
+ return Math.min(this.position + (Date.now() - this.lastUpdated), this.queue.current?.info.length ?? this.position);
47
70
  }
48
71
 
49
72
  // ─── Voice ───────────────────────────────────────────────────────
50
73
 
51
- private sendVoicePayload(channelId: string | null, selfDeaf: boolean, selfMute: boolean): void {
52
- this.manager.sendPayload(this.guildId, {
74
+ public sendVoicePayload(channelId: string | null, selfDeaf: boolean, selfMute: boolean): void {
75
+ this.manager.connector.sendPayload(this.guildId, {
53
76
  op: 4,
54
- d: {
55
- guild_id: this.guildId,
56
- channel_id: channelId,
57
- self_deaf: selfDeaf,
58
- self_mute: selfMute,
59
- },
77
+ d: { guild_id: this.guildId, channel_id: channelId, self_deaf: selfDeaf, self_mute: selfMute },
60
78
  });
61
79
  }
62
80
 
63
- /** Called when both sessionId and voiceToken/endpoint are available */
64
81
  public checkVoiceReady(): void {
65
82
  if (!this.sessionId || !this.voiceToken || !this.voiceEndpoint) return;
66
83
 
@@ -70,56 +87,76 @@ export class Player {
70
87
  sessionId: this.sessionId,
71
88
  };
72
89
 
73
- this.node
74
- .request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
75
- voice: voiceState,
76
- })
77
- .catch(console.error);
90
+ if (this.node.version === 4) {
91
+ this.node.rest.updatePlayer(this.guildId, { voice: voiceState }).catch(console.error);
92
+ } else {
93
+ this.node.send({
94
+ op: "voiceUpdate",
95
+ guildId: this.guildId,
96
+ sessionId: this.sessionId,
97
+ event: { token: this.voiceToken, guild_id: this.guildId, endpoint: this.voiceEndpoint },
98
+ });
99
+ }
100
+
101
+ this.state = PlayerState.CONNECTED;
78
102
  }
79
103
 
80
104
  // ─── Playback ─────────────────────────────────────────────────────
81
105
 
82
- /** Play a track */
83
- public async play(track: Track, options?: { startTime?: number; endTime?: number }): Promise<void> {
84
- this.queue.current = track;
106
+ public async play(track?: Track, options?: { startTime?: number; endTime?: number; noReplace?: boolean }): Promise<void> {
107
+ const toPlay = track ?? this.queue.current;
108
+ if (!toPlay) throw new PlayerError("No track to play");
85
109
 
86
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}?noReplace=false`, {
87
- track: { encoded: track.encoded },
88
- volume: this.volume,
89
- ...(options?.startTime && { position: options.startTime }),
90
- ...(options?.endTime && { endTime: options.endTime }),
91
- });
110
+ this.queue.current = toPlay;
111
+
112
+ if (this.node.version === 4) {
113
+ await this.node.rest.updatePlayer(this.guildId, {
114
+ track: { encoded: toPlay.encoded },
115
+ volume: this.volume,
116
+ ...(options?.startTime !== undefined && { position: options.startTime }),
117
+ ...(options?.endTime !== undefined && { endTime: options.endTime }),
118
+ }, options?.noReplace ?? false);
119
+ } else {
120
+ this.node.send({
121
+ op: "play",
122
+ guildId: this.guildId,
123
+ track: toPlay.encoded,
124
+ volume: String(this.volume),
125
+ ...(options?.startTime !== undefined && { startTime: String(options.startTime) }),
126
+ ...(options?.endTime !== undefined && { endTime: String(options.endTime) }),
127
+ ...(options?.noReplace !== undefined && { noReplace: options.noReplace }),
128
+ });
129
+ }
92
130
 
93
131
  this.playing = true;
94
132
  this.paused = false;
133
+ this.lastUpdated = Date.now();
95
134
  }
96
135
 
97
- /** Pause or resume playback */
98
136
  public async pause(state: boolean = true): Promise<void> {
99
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
100
- paused: state,
101
- });
137
+ if (this.node.version === 4) {
138
+ await this.node.rest.updatePlayer(this.guildId, { paused: state });
139
+ } else {
140
+ this.node.send({ op: "pause", guildId: this.guildId, pause: state });
141
+ }
102
142
  this.paused = state;
103
143
  this.playing = !state;
104
144
  }
105
145
 
106
- /** Resume playback */
107
- public async resume(): Promise<void> {
108
- return this.pause(false);
109
- }
146
+ public async resume(): Promise<void> { return this.pause(false); }
110
147
 
111
- /** Stop playback */
112
148
  public async stop(): Promise<void> {
113
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
114
- track: { encoded: null },
115
- });
149
+ if (this.node.version === 4) {
150
+ await this.node.rest.updatePlayer(this.guildId, { track: { encoded: null } });
151
+ } else {
152
+ this.node.send({ op: "stop", guildId: this.guildId });
153
+ }
116
154
  this.playing = false;
117
155
  this.paused = false;
118
156
  this.position = 0;
119
157
  this.queue.current = null;
120
158
  }
121
159
 
122
- /** Skip to next track */
123
160
  public async skip(): Promise<Track | null> {
124
161
  const next = this.queue.next();
125
162
  if (next) {
@@ -131,115 +168,170 @@ export class Player {
131
168
  return next;
132
169
  }
133
170
 
134
- /** Seek to position in milliseconds */
135
171
  public async seek(position: number): Promise<void> {
136
- if (!this.queue.current?.info.isSeekable) throw new Error("Current track is not seekable");
137
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
138
- position,
139
- });
172
+ if (!this.queue.current?.info.isSeekable) throw new PlayerError("Current track is not seekable");
173
+ if (position < 0 || position > (this.queue.current.info.length ?? Infinity)) {
174
+ throw new PlayerError(`Seek position out of range: ${position}`);
175
+ }
176
+ if (this.node.version === 4) {
177
+ await this.node.rest.updatePlayer(this.guildId, { position });
178
+ } else {
179
+ this.node.send({ op: "seek", guildId: this.guildId, position });
180
+ }
140
181
  this.position = position;
182
+ this.lastUpdated = Date.now();
141
183
  }
142
184
 
143
- /** Set volume (0–1000, Lavalink default 100) */
144
185
  public async setVolume(volume: number): Promise<void> {
145
- if (volume < 0 || volume > 1000) throw new Error("Volume must be between 0 and 1000");
146
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
147
- volume,
148
- });
186
+ if (volume < 0 || volume > 1000) throw new PlayerError("Volume must be between 0 and 1000");
187
+ if (this.node.version === 4) {
188
+ await this.node.rest.updatePlayer(this.guildId, { volume });
189
+ } else {
190
+ this.node.send({ op: "volume", guildId: this.guildId, volume });
191
+ }
149
192
  this.volume = volume;
150
193
  }
151
194
 
152
195
  // ─── Filters ──────────────────────────────────────────────────────
153
196
 
154
- /** Apply audio filters (bass boost, nightcore, 8D, etc.) */
155
- public async setFilters(filters: Record<string, unknown>): Promise<void> {
156
- this.filters = filters;
157
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
158
- filters,
159
- });
160
- }
197
+ public async setFilters(filters: FilterOptions): Promise<void> {
198
+ this.filters = { ...this.filters, ...filters };
161
199
 
162
- /** Enable bass boost */
163
- public async setBassBoost(level: "low" | "medium" | "high" | "off"): Promise<void> {
164
- const bands: { band: number; gain: number }[] = [];
165
- const gains = {
166
- off: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
167
- low: [0.2, 0.15, 0.1, 0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
168
- medium: [0.4, 0.3, 0.2, 0.1, 0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
169
- high: [0.6, 0.5, 0.4, 0.25, 0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
170
- };
200
+ if (this.node.version === 4) {
201
+ await this.node.rest.updatePlayer(this.guildId, { filters: this.filters });
202
+ } else {
203
+ if (filters.equalizer) {
204
+ this.node.send({ op: "equalizer", guildId: this.guildId, bands: filters.equalizer });
205
+ }
206
+ const rest = { ...filters };
207
+ delete rest.equalizer;
208
+ if (Object.keys(rest).length > 0) {
209
+ this.node.send({ op: "filters", guildId: this.guildId, ...rest });
210
+ }
211
+ }
212
+ }
171
213
 
172
- for (let i = 0; i < 15; i++) {
173
- bands.push({ band: i, gain: gains[level][i] });
214
+ public async clearFilters(): Promise<void> {
215
+ this.filters = {};
216
+ if (this.node.version === 4) {
217
+ await this.node.rest.updatePlayer(this.guildId, { filters: {} });
218
+ } else {
219
+ this.node.send({ op: "filters", guildId: this.guildId });
174
220
  }
221
+ }
222
+
223
+ // ─── Preset Filters ───────────────────────────────────────────────
175
224
 
176
- await this.setFilters({ ...this.filters, equalizer: bands });
225
+ public async setBassBoost(level: "off" | "low" | "medium" | "high" | "extreme"): Promise<void> {
226
+ const presets: Record<string, number[]> = {
227
+ off: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
228
+ low: [0.2, 0.15, 0.1, 0.05, 0.0, -0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0],
229
+ medium: [0.4, 0.3, 0.2, 0.1, 0.05, -0.05, 0, 0, 0, 0, 0, 0, 0, 0, 0],
230
+ high: [0.6, 0.5, 0.4, 0.25, 0.1, -0.1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
231
+ extreme: [1.0, 0.8, 0.6, 0.4, 0.2, -0.2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
232
+ };
233
+ await this.setFilters({ equalizer: presets[level].map((gain, band) => ({ band, gain })) });
177
234
  }
178
235
 
179
- /** Enable nightcore effect */
180
236
  public async setNightcore(enabled: boolean): Promise<void> {
181
- await this.setFilters({
182
- ...this.filters,
183
- timescale: enabled ? { speed: 1.2, pitch: 1.2, rate: 1.0 } : {},
184
- });
237
+ await this.setFilters({ timescale: enabled ? { speed: 1.2, pitch: 1.2, rate: 1.0 } : undefined });
238
+ }
239
+
240
+ public async setVaporwave(enabled: boolean): Promise<void> {
241
+ await this.setFilters({ timescale: enabled ? { speed: 0.8, pitch: 0.8, rate: 1.0 } : undefined });
185
242
  }
186
243
 
187
- /** Enable 8D audio */
188
244
  public async set8D(enabled: boolean): Promise<void> {
245
+ await this.setFilters({ rotation: enabled ? { rotationHz: 0.2 } : undefined });
246
+ }
247
+
248
+ public async setKaraoke(enabled: boolean): Promise<void> {
189
249
  await this.setFilters({
190
- ...this.filters,
191
- rotation: enabled ? { rotationHz: 0.2 } : {},
250
+ karaoke: enabled ? { level: 1.0, monoLevel: 1.0, filterBand: 220.0, filterWidth: 100.0 } : undefined,
192
251
  });
193
252
  }
194
253
 
195
- /** Clear all filters */
196
- public async clearFilters(): Promise<void> {
197
- this.filters = {};
198
- await this.node.request("PATCH", `/sessions/${this.node.sessionId}/players/${this.guildId}`, {
199
- filters: {},
200
- });
254
+ public async setTremolo(enabled: boolean, frequency = 2.0, depth = 0.5): Promise<void> {
255
+ await this.setFilters({ tremolo: enabled ? { frequency, depth } : undefined });
256
+ }
257
+
258
+ public async setVibrato(enabled: boolean, frequency = 2.0, depth = 0.5): Promise<void> {
259
+ await this.setFilters({ vibrato: enabled ? { frequency, depth } : undefined });
260
+ }
261
+
262
+ public async setLowPass(enabled: boolean, smoothing = 20.0): Promise<void> {
263
+ await this.setFilters({ lowPass: enabled ? { smoothing } : undefined });
201
264
  }
202
265
 
203
266
  // ─── Queue Helpers ────────────────────────────────────────────────
204
267
 
205
- /** Add track(s) to queue and optionally start playing */
206
- public async add(tracks: Track | Track[], playNow: boolean = false): Promise<void> {
268
+ public async add(tracks: Track | Track[], requester?: unknown): Promise<void> {
207
269
  const arr = Array.isArray(tracks) ? tracks : [tracks];
208
270
 
271
+ // Attach requester to each track
272
+ if (requester) arr.forEach(t => { (t as any).requester = requester; });
273
+
209
274
  if (!this.queue.current && arr.length > 0) {
210
275
  const first = arr.shift()!;
211
- this.queue.current = first;
212
276
  this.queue.add(arr);
213
277
  await this.play(first);
214
278
  } else {
215
279
  this.queue.add(arr);
216
- if (playNow && arr.length > 0) {
217
- await this.play(arr[0]);
218
- }
219
280
  }
220
281
  }
221
282
 
222
- /** Set loop mode */
223
283
  public setLoop(mode: "none" | "track" | "queue"): void {
224
284
  this.loop = mode;
225
285
  }
226
286
 
227
- // ─── Disconnect ───────────────────────────────────────────────────
287
+ public setAutoplay(enabled: boolean): void {
288
+ this.autoplay = enabled;
289
+ }
290
+
291
+ // ─── Node Moving ──────────────────────────────────────────────────
292
+
293
+ /** Move player to a different Lavalink node */
294
+ public async moveToNode(node: Node): Promise<void> {
295
+ if (this.node === node) return;
296
+ const oldNode = this.node;
297
+ this.node = node;
298
+
299
+ // Destroy on old node
300
+ await oldNode.rest.destroyPlayer(this.guildId).catch(() => {});
301
+
302
+ // Recreate on new node with current state
303
+ if (this.queue.current) {
304
+ await this.play(this.queue.current, { startTime: this.realPosition });
305
+ }
306
+
307
+ // Reapply filters and volume
308
+ if (Object.keys(this.filters).length > 0) {
309
+ await this.setFilters(this.filters).catch(() => {});
310
+ }
311
+ if (this.volume !== 100) {
312
+ await this.setVolume(this.volume).catch(() => {});
313
+ }
314
+ }
315
+
316
+ // ─── Voice Channel Moves ──────────────────────────────────────────
317
+
318
+ public async move(channelId: string): Promise<void> {
319
+ this.voiceChannelId = channelId;
320
+ this.sendVoicePayload(channelId, this.selfDeaf, this.selfMute);
321
+ }
322
+
323
+ // ─── Destroy ──────────────────────────────────────────────────────
228
324
 
229
- /** Disconnect from voice and clean up */
230
325
  public async destroy(): Promise<void> {
326
+ this.state = PlayerState.DISCONNECTING;
231
327
  this.sendVoicePayload(null, false, false);
232
- await this.node
233
- .request("DELETE", `/sessions/${this.node.sessionId}/players/${this.guildId}`)
234
- .catch(() => {});
328
+
329
+ await this.node.rest.destroyPlayer(this.guildId).catch(() => {});
330
+
235
331
  this.playing = false;
236
- this.connected = false;
332
+ this.paused = false;
237
333
  this.queue.clear();
334
+ this.data.clear();
335
+ this.state = PlayerState.DISCONNECTED;
238
336
  }
239
-
240
- /** Move to a different voice channel */
241
- public async move(channelId: string, selfDeaf: boolean = true): Promise<void> {
242
- this.voiceChannelId = channelId;
243
- this.sendVoicePayload(channelId, selfDeaf, false);
244
- }
245
- }
337
+ }
package/src/Plugin.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Yukimu } from "./Yukimu";
2
+
3
+ /**
4
+ * Base Plugin class — extend this to create Yukimu plugins.
5
+ * Similar to Kazagumo's plugin system.
6
+ *
7
+ * Example:
8
+ * class MyPlugin extends Plugin {
9
+ * public name = "MyPlugin";
10
+ * public load(manager: Yukimu) {
11
+ * manager.on("trackStart", (player, track) => { ... });
12
+ * }
13
+ * }
14
+ */
15
+ export abstract class Plugin {
16
+ public abstract name: string;
17
+
18
+ /** Called when plugin is loaded into Yukimu */
19
+ public abstract load(manager: Yukimu): void;
20
+
21
+ /** Called when plugin is unloaded */
22
+ public unload?(manager: Yukimu): void;
23
+ }
package/src/Queue.ts CHANGED
@@ -1,47 +1,48 @@
1
- import { Track } from "./types";
1
+ import type { Track } from "./types";
2
2
 
3
3
  export class Queue {
4
4
  public current: Track | null = null;
5
- public previous: Track | null = null;
5
+ public previous: Track[] = [];
6
6
  private tracks: Track[] = [];
7
7
 
8
- // ─── Basic Operations ─────────────────────────────────────────────
8
+ // ─── Add / Remove ─────────────────────────────────────────────────
9
9
 
10
- /** Add tracks to the end of the queue */
11
- public add(tracks: Track | Track[]): void {
10
+ public add(tracks: Track | Track[], position?: number): void {
12
11
  const arr = Array.isArray(tracks) ? tracks : [tracks];
13
- this.tracks.push(...arr);
12
+ if (position !== undefined) {
13
+ this.tracks.splice(position, 0, ...arr);
14
+ } else {
15
+ this.tracks.push(...arr);
16
+ }
14
17
  }
15
18
 
16
- /** Remove and return the next track */
17
19
  public next(): Track | null {
18
- if (this.current) this.previous = this.current;
20
+ if (this.current) {
21
+ this.previous.unshift(this.current);
22
+ if (this.previous.length > 10) this.previous.pop(); // keep last 10
23
+ }
19
24
  this.current = this.tracks.shift() ?? null;
20
25
  return this.current;
21
26
  }
22
27
 
23
- /** Peek at upcoming tracks without modifying queue */
24
- public peek(count: number = 5): Track[] {
25
- return this.tracks.slice(0, count);
26
- }
27
-
28
- /** Remove a track at a specific index */
29
28
  public remove(index: number): Track | null {
30
29
  if (index < 0 || index >= this.tracks.length) return null;
31
30
  const [removed] = this.tracks.splice(index, 1);
32
31
  return removed;
33
32
  }
34
33
 
35
- /** Clear all queued tracks (does not affect current) */
34
+ public removeRange(start: number, end: number): Track[] {
35
+ return this.tracks.splice(start, end - start);
36
+ }
37
+
36
38
  public clear(): void {
37
39
  this.tracks = [];
38
40
  this.current = null;
39
- this.previous = null;
41
+ this.previous = [];
40
42
  }
41
43
 
42
- // ─── Shuffle ──────────────────────────────────────────────────────
44
+ // ─── Reorder ──────────────────────────────────────────────────────
43
45
 
44
- /** Fisher-Yates shuffle */
45
46
  public shuffle(): void {
46
47
  for (let i = this.tracks.length - 1; i > 0; i--) {
47
48
  const j = Math.floor(Math.random() * (i + 1));
@@ -49,34 +50,42 @@ export class Queue {
49
50
  }
50
51
  }
51
52
 
52
- // ─── Move / Reorder ───────────────────────────────────────────────
53
-
54
- /** Move a track from one position to another */
55
53
  public move(from: number, to: number): void {
56
54
  if (from < 0 || to < 0 || from >= this.tracks.length || to >= this.tracks.length) return;
57
55
  const [track] = this.tracks.splice(from, 1);
58
56
  this.tracks.splice(to, 0, track);
59
57
  }
60
58
 
61
- // ─── Getters ──────────────────────────────────────────────────────
59
+ /** Skip to a specific position in queue */
60
+ public skipto(index: number): Track | null {
61
+ if (index < 0 || index >= this.tracks.length) return null;
62
+ this.tracks.splice(0, index);
63
+ return this.next();
64
+ }
65
+
66
+ // ─── Peek / Find ──────────────────────────────────────────────────
62
67
 
63
- /** Total tracks in queue (not including current) */
64
- get size(): number {
65
- return this.tracks.length;
68
+ public peek(count: number = 10): Track[] {
69
+ return this.tracks.slice(0, count);
66
70
  }
67
71
 
68
- /** Total duration of all queued tracks in ms */
69
- get totalDuration(): number {
70
- return this.tracks.reduce((acc, t) => acc + (t.info.length ?? 0), 0);
72
+ public find(predicate: (track: Track) => boolean): Track | undefined {
73
+ return this.tracks.find(predicate);
71
74
  }
72
75
 
73
- /** Whether the queue is empty */
74
- get isEmpty(): boolean {
75
- return this.tracks.length === 0;
76
+ public filter(predicate: (track: Track) => boolean): Track[] {
77
+ return this.tracks.filter(predicate);
76
78
  }
77
79
 
78
- /** All tracks as array (read-only view) */
79
- get list(): ReadonlyArray<Track> {
80
- return this.tracks;
80
+ // ─── Getters ──────────────────────────────────────────────────────
81
+
82
+ get size(): number { return this.tracks.length; }
83
+
84
+ get isEmpty(): boolean { return this.tracks.length === 0; }
85
+
86
+ get totalDuration(): number {
87
+ return this.tracks.reduce((acc, t) => acc + (t.info?.length ?? 0), 0);
81
88
  }
89
+
90
+ get list(): ReadonlyArray<Track> { return this.tracks; }
82
91
  }