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.
- package/.cache/replit/env/latest +88 -0
- package/.cache/replit/env/latest.json +1 -0
- package/.cache/replit/modules/python-3.11.res +1 -0
- package/.cache/replit/nix/dotreplitenv.json +1 -1
- package/.cache/replit/toolchain.json +1 -1
- package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.packager.installForAll.0 +1 -0
- package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.shell.exec.1 +1 -0
- package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.packager.installForAll.0 +1 -0
- package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.shell.exec.1 +1 -0
- package/.replit +4 -1
- package/.upm/store.json +1 -1
- package/dist/ConnectionPool.d.ts +34 -0
- package/dist/ConnectionPool.d.ts.map +1 -0
- package/dist/ConnectionPool.js +124 -0
- package/dist/ConnectionPool.js.map +1 -0
- package/dist/Constants.d.ts +66 -0
- package/dist/Constants.d.ts.map +1 -0
- package/dist/Constants.js +101 -0
- package/dist/Constants.js.map +1 -0
- package/dist/Node.d.ts +26 -10
- package/dist/Node.d.ts.map +1 -1
- package/dist/Node.js +203 -81
- package/dist/Node.js.map +1 -1
- package/dist/Player.d.ts +30 -29
- package/dist/Player.d.ts.map +1 -1
- package/dist/Player.js +178 -97
- package/dist/Player.js.map +1 -1
- package/dist/Plugin.d.ts +21 -0
- package/dist/Plugin.d.ts.map +1 -0
- package/dist/Plugin.js +19 -0
- package/dist/Plugin.js.map +1 -0
- package/dist/Queue.d.ts +10 -16
- package/dist/Queue.d.ts.map +1 -1
- package/dist/Queue.js +39 -32
- package/dist/Queue.js.map +1 -1
- package/dist/Resolver.d.ts +4 -21
- package/dist/Resolver.d.ts.map +1 -1
- package/dist/Resolver.js +28 -66
- package/dist/Resolver.js.map +1 -1
- package/dist/Rest.d.ts +24 -0
- package/dist/Rest.d.ts.map +1 -0
- package/dist/Rest.js +104 -0
- package/dist/Rest.js.map +1 -0
- package/dist/TrackCache.d.ts +21 -0
- package/dist/TrackCache.d.ts.map +1 -0
- package/dist/TrackCache.js +57 -0
- package/dist/TrackCache.js.map +1 -0
- package/dist/WsQueue.d.ts +20 -0
- package/dist/WsQueue.d.ts.map +1 -0
- package/dist/WsQueue.js +74 -0
- package/dist/WsQueue.js.map +1 -0
- package/dist/Yukimu.d.ts +33 -32
- package/dist/Yukimu.d.ts.map +1 -1
- package/dist/Yukimu.js +133 -65
- package/dist/Yukimu.js.map +1 -1
- package/dist/connector/Connector.d.ts +8 -0
- package/dist/connector/Connector.d.ts.map +1 -0
- package/dist/connector/Connector.js +11 -0
- package/dist/connector/Connector.js.map +1 -0
- package/dist/connector/DiscordJS.d.ts +25 -0
- package/dist/connector/DiscordJS.d.ts.map +1 -0
- package/dist/connector/DiscordJS.js +27 -0
- package/dist/connector/DiscordJS.js.map +1 -0
- package/dist/connector/Eris.d.ts +23 -0
- package/dist/connector/Eris.d.ts.map +1 -0
- package/dist/connector/Eris.js +30 -0
- package/dist/connector/Eris.js.map +1 -0
- package/dist/connector/Oceanic.d.ts +20 -0
- package/dist/connector/Oceanic.d.ts.map +1 -0
- package/dist/connector/Oceanic.js +27 -0
- package/dist/connector/Oceanic.js.map +1 -0
- package/dist/errors/YukimuError.d.ts +15 -0
- package/dist/errors/YukimuError.d.ts.map +1 -0
- package/dist/errors/YukimuError.js +35 -0
- package/dist/errors/YukimuError.js.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/AutoResume.d.ts +21 -0
- package/dist/plugins/AutoResume.d.ts.map +1 -0
- package/dist/plugins/AutoResume.js +52 -0
- package/dist/plugins/AutoResume.js.map +1 -0
- package/dist/plugins/PlayerMoved.d.ts +7 -0
- package/dist/plugins/PlayerMoved.d.ts.map +1 -0
- package/dist/plugins/PlayerMoved.js +30 -0
- package/dist/plugins/PlayerMoved.js.map +1 -0
- package/dist/types.d.ts +92 -38
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -12
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/ConnectionPool.ts +131 -0
- package/src/Constants.ts +101 -0
- package/src/Node.ts +157 -174
- package/src/Player.ts +200 -108
- package/src/Plugin.ts +23 -0
- package/src/Queue.ts +43 -34
- package/src/Resolver.ts +29 -77
- package/src/Rest.ts +121 -0
- package/src/TrackCache.ts +69 -0
- package/src/WsQueue.ts +78 -0
- package/src/Yukimu.ts +156 -85
- package/src/connector/Connector.ts +13 -0
- package/src/connector/DiscordJS.ts +33 -0
- package/src/connector/Eris.ts +35 -0
- package/src/connector/Oceanic.ts +32 -0
- package/src/errors/YukimuError.ts +33 -0
- package/src/index.ts +41 -1
- package/src/plugins/AutoResume.ts +61 -0
- package/src/plugins/PlayerMoved.ts +26 -0
- package/src/types.ts +50 -83
- package/tsconfig.json +4 -2
- package/README.md +0 -152
package/src/Player.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Queue } from "./Queue";
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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:
|
|
36
|
+
public filters: FilterOptions = {};
|
|
31
37
|
|
|
32
|
-
//
|
|
33
|
-
public
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
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
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
328
|
+
|
|
329
|
+
await this.node.rest.destroyPlayer(this.guildId).catch(() => {});
|
|
330
|
+
|
|
235
331
|
this.playing = false;
|
|
236
|
-
this.
|
|
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
|
|
5
|
+
public previous: Track[] = [];
|
|
6
6
|
private tracks: Track[] = [];
|
|
7
7
|
|
|
8
|
-
// ───
|
|
8
|
+
// ─── Add / Remove ─────────────────────────────────────────────────
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
41
|
+
this.previous = [];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
// ───
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
return this.tracks.length;
|
|
68
|
+
public peek(count: number = 10): Track[] {
|
|
69
|
+
return this.tracks.slice(0, count);
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
}
|