ryanlink 1.0.1
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/LICENSE +37 -0
- package/README.md +455 -0
- package/dist/index.d.mts +1335 -0
- package/dist/index.d.ts +1335 -0
- package/dist/index.js +4694 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4604 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
- package/src/audio/AudioFilters.ts +316 -0
- package/src/audio/AudioQueue.ts +782 -0
- package/src/audio/AudioTrack.ts +242 -0
- package/src/audio/QueueController.ts +252 -0
- package/src/audio/TrackCollection.ts +138 -0
- package/src/audio/index.ts +9 -0
- package/src/config/defaults.ts +223 -0
- package/src/config/endpoints.ts +99 -0
- package/src/config/index.ts +9 -0
- package/src/config/patterns.ts +55 -0
- package/src/config/presets.ts +400 -0
- package/src/config/symbols.ts +31 -0
- package/src/core/PluginSystem.ts +50 -0
- package/src/core/RyanlinkPlayer.ts +403 -0
- package/src/core/index.ts +6 -0
- package/src/extensions/AutoplayExtension.ts +283 -0
- package/src/extensions/FairPlayExtension.ts +154 -0
- package/src/extensions/LyricsExtension.ts +187 -0
- package/src/extensions/PersistenceExtension.ts +182 -0
- package/src/extensions/SponsorBlockExtension.ts +81 -0
- package/src/extensions/index.ts +9 -0
- package/src/index.ts +19 -0
- package/src/lavalink/ConnectionPool.ts +326 -0
- package/src/lavalink/HttpClient.ts +316 -0
- package/src/lavalink/LavalinkConnection.ts +409 -0
- package/src/lavalink/index.ts +7 -0
- package/src/metadata.ts +88 -0
- package/src/types/api/Rest.ts +949 -0
- package/src/types/api/Websocket.ts +463 -0
- package/src/types/api/index.ts +6 -0
- package/src/types/audio/FilterManager.ts +29 -0
- package/src/types/audio/Queue.ts +4 -0
- package/src/types/audio/QueueManager.ts +30 -0
- package/src/types/audio/index.ts +7 -0
- package/src/types/common.ts +63 -0
- package/src/types/core/Player.ts +322 -0
- package/src/types/core/index.ts +5 -0
- package/src/types/index.ts +6 -0
- package/src/types/lavalink/Node.ts +173 -0
- package/src/types/lavalink/NodeManager.ts +34 -0
- package/src/types/lavalink/REST.ts +144 -0
- package/src/types/lavalink/index.ts +32 -0
- package/src/types/voice/VoiceManager.ts +176 -0
- package/src/types/voice/index.ts +5 -0
- package/src/utils/helpers.ts +169 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/validators.ts +184 -0
- package/src/voice/RegionSelector.ts +184 -0
- package/src/voice/VoiceConnection.ts +451 -0
- package/src/voice/VoiceSession.ts +297 -0
- package/src/voice/index.ts +7 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
import { LookupSymbol, UpdateSymbol } from "../config/symbols";
|
|
2
|
+
import { FilterManager } from "./AudioFilters";
|
|
3
|
+
import { Track } from "./AudioTrack";
|
|
4
|
+
import { Playlist } from "./TrackCollection";
|
|
5
|
+
import type { Player } from "../core/RyanlinkPlayer";
|
|
6
|
+
import type { VoiceState } from "../voice/VoiceSession";
|
|
7
|
+
import type {
|
|
8
|
+
APIPlayer,
|
|
9
|
+
QueueContext,
|
|
10
|
+
RepeatMode,
|
|
11
|
+
SearchResult,
|
|
12
|
+
PlayerUpdateRequestBody,
|
|
13
|
+
PlayerUpdateQueryParams,
|
|
14
|
+
JsonObject,
|
|
15
|
+
Severity,
|
|
16
|
+
} from "../types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a music queue for a guild
|
|
20
|
+
* Manages tracks, playback state, and filters
|
|
21
|
+
*/
|
|
22
|
+
export class Queue<Context extends Record<string, unknown> = QueueContext> {
|
|
23
|
+
#player: APIPlayer;
|
|
24
|
+
|
|
25
|
+
#autoplay = false;
|
|
26
|
+
#repeatMode: RepeatMode = "none";
|
|
27
|
+
|
|
28
|
+
#tracks: Track[] = [];
|
|
29
|
+
#previousTracks: Track[] = [];
|
|
30
|
+
|
|
31
|
+
context = {} as Context;
|
|
32
|
+
|
|
33
|
+
readonly voice: VoiceState;
|
|
34
|
+
readonly filters: FilterManager;
|
|
35
|
+
readonly player: Player;
|
|
36
|
+
|
|
37
|
+
constructor(player: Player, guildId: string, context?: Context) {
|
|
38
|
+
if (player.queues.has(guildId)) {
|
|
39
|
+
throw new Error("An identical queue already exists");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const _player = player.queues[LookupSymbol](guildId);
|
|
43
|
+
if (!_player) {
|
|
44
|
+
throw new Error(`No player found for guild '${guildId}'`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const voice = player.voices.get(guildId);
|
|
48
|
+
if (!voice) {
|
|
49
|
+
throw new Error(`No connection found for guild '${guildId}'`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.#player = _player;
|
|
53
|
+
if (context !== undefined) {
|
|
54
|
+
this.context = context;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.voice = voice;
|
|
58
|
+
this.filters = new FilterManager(player, guildId);
|
|
59
|
+
this.player = player;
|
|
60
|
+
|
|
61
|
+
// Make properties immutable
|
|
62
|
+
const immutable: PropertyDescriptor = {
|
|
63
|
+
writable: false,
|
|
64
|
+
configurable: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
Object.defineProperties(this, {
|
|
68
|
+
voice: immutable,
|
|
69
|
+
filters: immutable,
|
|
70
|
+
player: { ...immutable, enumerable: false },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get node() {
|
|
75
|
+
return this.voice.nodeSessionId
|
|
76
|
+
? this.player.nodes.all.find((n) => n.sessionId === this.voice.nodeSessionId)
|
|
77
|
+
: this.player.nodes.relevant()[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get rest() {
|
|
81
|
+
return this.node?.rest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get guildId(): string {
|
|
85
|
+
return this.voice.guildId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get volume(): number {
|
|
89
|
+
return this.#player.volume;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get paused(): boolean {
|
|
93
|
+
return this.#player.paused;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get stopped(): boolean {
|
|
97
|
+
return this.track !== null && this.#player.track === null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get empty(): boolean {
|
|
101
|
+
return this.finished && !this.hasPrevious;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get playing(): boolean {
|
|
105
|
+
return !this.paused && this.track !== null && this.#player.track !== null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get autoplay(): boolean {
|
|
109
|
+
return this.#autoplay;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get finished(): boolean {
|
|
113
|
+
return this.#tracks.length === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get destroyed(): boolean {
|
|
117
|
+
return this.player.queues.get(this.guildId) !== this;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get repeatMode(): RepeatMode {
|
|
121
|
+
return this.#repeatMode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get hasNext(): boolean {
|
|
125
|
+
return this.#tracks.length > 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get hasPrevious(): boolean {
|
|
129
|
+
return this.#previousTracks.length !== 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get track(): Track | null {
|
|
133
|
+
return this.#tracks[0] ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get previousTrack(): Track | null {
|
|
137
|
+
return this.#previousTracks[this.#previousTracks.length - 1] ?? null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get tracks(): Track[] {
|
|
141
|
+
return this.#tracks;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get previousTracks(): Track[] {
|
|
145
|
+
return this.#previousTracks;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get length(): number {
|
|
149
|
+
return this.#tracks.length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get totalLength(): number {
|
|
153
|
+
return this.length + this.#previousTracks.length;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get duration(): number {
|
|
157
|
+
return this.#tracks.reduce((time, track) => time + (track.isLive ? 0 : track.duration), 0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get formattedDuration(): string {
|
|
161
|
+
return this.#formatDuration(this.duration);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get currentTime(): number {
|
|
165
|
+
if (this.#player.paused || !this.#player.state.connected) {
|
|
166
|
+
return this.#player.state.position;
|
|
167
|
+
}
|
|
168
|
+
if (this.#player.state.position === 0) {
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
return this.#player.state.position + (Date.now() - this.#player.state.time);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get formattedCurrentTime(): string {
|
|
175
|
+
return this.#formatDuration(this.currentTime);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#formatDuration(ms: number): string {
|
|
179
|
+
const seconds = Math.floor(ms / 1000);
|
|
180
|
+
const minutes = Math.floor(seconds / 60);
|
|
181
|
+
const hours = Math.floor(minutes / 60);
|
|
182
|
+
|
|
183
|
+
if (hours > 0) {
|
|
184
|
+
return `${hours}:${(minutes % 60).toString().padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
185
|
+
}
|
|
186
|
+
return `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#error(data: string | { message?: string; cause?: string; severity?: Severity }): Error {
|
|
190
|
+
const explicit = typeof data === "string";
|
|
191
|
+
const message = explicit ? data : (data.message ?? data.cause ?? "Unknown error");
|
|
192
|
+
const error = new Error(message) as Error & { severity?: Severity };
|
|
193
|
+
error.name = `Error [${this.constructor.name}]`;
|
|
194
|
+
if (!explicit && data.severity) {
|
|
195
|
+
error.severity = data.severity;
|
|
196
|
+
}
|
|
197
|
+
return error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async #update(data: PlayerUpdateRequestBody, params?: PlayerUpdateQueryParams): Promise<void> {
|
|
201
|
+
const node = this.node;
|
|
202
|
+
if (!node) {
|
|
203
|
+
throw this.#error("No node available");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const player = await node.rest.updatePlayer(this.guildId, data, params);
|
|
207
|
+
Object.assign(this.#player, player);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Sync queue state with Lavalink
|
|
212
|
+
* @param target - "local" to pull from Lavalink, "remote" to push to Lavalink
|
|
213
|
+
*/
|
|
214
|
+
async sync(target: "local" | "remote" = "local"): Promise<void> {
|
|
215
|
+
const node = this.node;
|
|
216
|
+
if (!node) {
|
|
217
|
+
throw this.#error("No node available");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (target === "local") {
|
|
221
|
+
const player = await node.rest.fetchPlayer(this.guildId);
|
|
222
|
+
Object.assign(this.#player, player);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (target !== "remote") {
|
|
227
|
+
throw this.#error("Target must be 'local' or 'remote'");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const voice = this.player.voices[LookupSymbol](this.guildId);
|
|
231
|
+
if (!voice) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const request: PlayerUpdateRequestBody = {
|
|
236
|
+
voice: {
|
|
237
|
+
token: voice.token,
|
|
238
|
+
endpoint: voice.endpoint,
|
|
239
|
+
sessionId: voice.session_id,
|
|
240
|
+
channelId: voice.channel_id,
|
|
241
|
+
},
|
|
242
|
+
filters: this.#player.filters,
|
|
243
|
+
paused: this.#player.paused,
|
|
244
|
+
volume: this.#player.volume,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (this.#player.track !== null) {
|
|
248
|
+
request.track = {
|
|
249
|
+
encoded: this.#player.track.encoded,
|
|
250
|
+
userData: this.#player.track.userData,
|
|
251
|
+
};
|
|
252
|
+
request.position = this.#player.state.position;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await this.#update(request);
|
|
256
|
+
const nodeSessionId = this.node?.sessionId ?? "";
|
|
257
|
+
this.player.voices[UpdateSymbol](this.guildId, {
|
|
258
|
+
node_session_id: nodeSessionId,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Search for tracks
|
|
264
|
+
*/
|
|
265
|
+
async search(query: string, prefix = this.player.options.queryPrefix): Promise<SearchResult> {
|
|
266
|
+
return this.player.search(query, { prefix, node: this.node?.name });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Add tracks to the queue
|
|
271
|
+
*/
|
|
272
|
+
add(source: Track | Track[] | Playlist, userData?: JsonObject): this {
|
|
273
|
+
const added: Track[] = [];
|
|
274
|
+
|
|
275
|
+
if (source instanceof Track) {
|
|
276
|
+
Object.assign(source.userData, userData);
|
|
277
|
+
this.#tracks.push(source);
|
|
278
|
+
added.push(source);
|
|
279
|
+
} else if (source instanceof Playlist) {
|
|
280
|
+
for (const track of source.tracks) {
|
|
281
|
+
Object.assign(track.userData, userData);
|
|
282
|
+
this.#tracks.push(track);
|
|
283
|
+
added.push(track);
|
|
284
|
+
}
|
|
285
|
+
} else if (Array.isArray(source)) {
|
|
286
|
+
for (const track of source) {
|
|
287
|
+
if (track instanceof Track) {
|
|
288
|
+
Object.assign(track.userData, userData);
|
|
289
|
+
this.#tracks.push(track);
|
|
290
|
+
added.push(track);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
throw this.#error("Source must be a track, playlist, or array of tracks");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.player.emit("trackAdd", this.player, this.guildId, added);
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Add related tracks (for autoplay)
|
|
303
|
+
*/
|
|
304
|
+
async addRelated(refTrack?: Track): Promise<Track[]> {
|
|
305
|
+
refTrack ??= this.track ?? this.previousTrack ?? undefined;
|
|
306
|
+
if (!refTrack) {
|
|
307
|
+
throw this.#error("The queue is empty and there is no track to refer");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!this.node) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
const relatedTracks = await this.player.options.fetchRelatedTracks?.(this, refTrack);
|
|
314
|
+
this.add(relatedTracks);
|
|
315
|
+
return relatedTracks;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Remove tracks from the queue
|
|
320
|
+
*/
|
|
321
|
+
remove(index: number): Track | undefined;
|
|
322
|
+
remove(indices: number[]): Track[];
|
|
323
|
+
remove(input: number | number[]): Track | Track[] | undefined {
|
|
324
|
+
if (typeof input === "number") {
|
|
325
|
+
if (input === 0 && !this.stopped) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (input < 0) {
|
|
329
|
+
return this.#previousTracks.splice(input, 1)[0];
|
|
330
|
+
}
|
|
331
|
+
return this.#tracks.splice(input, 1)[0];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (Array.isArray(input)) {
|
|
335
|
+
if (input.length === 0) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const tracks: Track[] = [];
|
|
339
|
+
|
|
340
|
+
const indices = input.toSorted((a, b) => a - b);
|
|
341
|
+
for (let i = 0; i < indices.length; i++) {
|
|
342
|
+
const index = indices[i] - i;
|
|
343
|
+
if (index === 0 && !this.stopped) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (index < 0) {
|
|
347
|
+
tracks.push(...this.#previousTracks.splice(index, 1));
|
|
348
|
+
} else if (index < this.#tracks.length) {
|
|
349
|
+
tracks.push(...this.#tracks.splice(index, 1));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return tracks;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
throw this.#error("Input must be an index or array of indices");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Clear tracks from the queue
|
|
360
|
+
*/
|
|
361
|
+
clear(type?: "current" | "previous"): void {
|
|
362
|
+
switch (type) {
|
|
363
|
+
case "current":
|
|
364
|
+
if (!this.finished) {
|
|
365
|
+
this.#tracks.length = this.stopped ? 0 : 1;
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
case "previous":
|
|
369
|
+
this.#previousTracks.length = 0;
|
|
370
|
+
break;
|
|
371
|
+
default:
|
|
372
|
+
if (!this.finished) {
|
|
373
|
+
this.#tracks.length = this.stopped ? 0 : 1;
|
|
374
|
+
}
|
|
375
|
+
this.#previousTracks.length = 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Jump to a specific track
|
|
381
|
+
*/
|
|
382
|
+
async jump(index: number): Promise<Track> {
|
|
383
|
+
if (this.empty) {
|
|
384
|
+
throw this.#error("The queue is empty at the moment");
|
|
385
|
+
}
|
|
386
|
+
if (!Number.isInteger(index)) {
|
|
387
|
+
throw this.#error("Index must be an integer");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const track = index < 0 ? this.#previousTracks[this.#previousTracks.length + index] : this.#tracks[index];
|
|
391
|
+
|
|
392
|
+
if (!track) {
|
|
393
|
+
throw this.#error("Specified index is out of range");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (index < 0) {
|
|
397
|
+
this.#tracks.unshift(...this.#previousTracks.splice(index));
|
|
398
|
+
} else {
|
|
399
|
+
this.#previousTracks.push(...this.#tracks.splice(0, index));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await this.#update({
|
|
403
|
+
paused: false,
|
|
404
|
+
track: { encoded: track.encoded, userData: track.userData },
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return track;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Pause playback
|
|
412
|
+
*/
|
|
413
|
+
async pause(): Promise<boolean> {
|
|
414
|
+
await this.#update({ paused: true });
|
|
415
|
+
return this.#player.paused;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Resume playback
|
|
420
|
+
*/
|
|
421
|
+
async resume(): Promise<boolean> {
|
|
422
|
+
if (this.stopped) {
|
|
423
|
+
await this.jump(0);
|
|
424
|
+
} else {
|
|
425
|
+
await this.#update({ paused: false });
|
|
426
|
+
}
|
|
427
|
+
return !this.#player.paused;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Seek to a position
|
|
432
|
+
*/
|
|
433
|
+
async seek(ms: number): Promise<number> {
|
|
434
|
+
if (this.track === null) {
|
|
435
|
+
throw this.#error("No track is playing at the moment");
|
|
436
|
+
}
|
|
437
|
+
if (!this.track.isSeekable) {
|
|
438
|
+
throw this.#error("Current track is not seekable");
|
|
439
|
+
}
|
|
440
|
+
if (!Number.isInteger(ms) || ms < 0) {
|
|
441
|
+
throw this.#error("Seek time must be a positive integer");
|
|
442
|
+
}
|
|
443
|
+
if (ms > this.track.duration) {
|
|
444
|
+
throw this.#error("Specified time to seek is out of range");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const body: PlayerUpdateRequestBody = { paused: false, position: ms };
|
|
448
|
+
|
|
449
|
+
if (this.#player.track?.info.identifier !== this.track.id) {
|
|
450
|
+
body.track = { encoded: this.track.encoded, userData: this.track.userData };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
await this.#update(body);
|
|
454
|
+
return this.#player.state.position;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Play next track
|
|
459
|
+
*/
|
|
460
|
+
async next(): Promise<Track | null> {
|
|
461
|
+
if (this.hasNext) {
|
|
462
|
+
return this.jump(1);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (this.hasPrevious && this.#repeatMode === "queue") {
|
|
466
|
+
const track = this.#previousTracks.shift();
|
|
467
|
+
if (track) {
|
|
468
|
+
this.#tracks.push(track);
|
|
469
|
+
}
|
|
470
|
+
return this.jump(this.hasNext ? 1 : 0);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!this.empty && this.#autoplay) {
|
|
474
|
+
const related = await this.addRelated();
|
|
475
|
+
if (related.length > 0) {
|
|
476
|
+
return this.jump(this.length - related.length);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!this.finished) {
|
|
481
|
+
const track = this.#tracks.shift();
|
|
482
|
+
if (track) {
|
|
483
|
+
this.#previousTracks.push(track);
|
|
484
|
+
}
|
|
485
|
+
await this.stop();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Play previous track
|
|
493
|
+
*/
|
|
494
|
+
async previous(): Promise<Track | null> {
|
|
495
|
+
if (this.hasPrevious) {
|
|
496
|
+
return this.jump(-1);
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Shuffle tracks
|
|
503
|
+
*/
|
|
504
|
+
shuffle(includePrevious = false): this {
|
|
505
|
+
if (includePrevious === true) {
|
|
506
|
+
this.#tracks.push(...this.#previousTracks.splice(0));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (this.#tracks.length < 3) {
|
|
510
|
+
return this;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (let i = this.#tracks.length - 1; i > 1; --i) {
|
|
514
|
+
const j = Math.floor(Math.random() * i) + 1;
|
|
515
|
+
[this.#tracks[i], this.#tracks[j]] = [this.#tracks[j], this.#tracks[i]];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Set volume
|
|
523
|
+
*/
|
|
524
|
+
async setVolume(volume: number): Promise<number> {
|
|
525
|
+
if (!Number.isInteger(volume) || volume < 0) {
|
|
526
|
+
throw this.#error("Volume must be a positive integer");
|
|
527
|
+
}
|
|
528
|
+
if (volume > 1000) {
|
|
529
|
+
throw this.#error("Volume cannot be more than 1000");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
await this.#update({ volume });
|
|
533
|
+
return this.#player.volume;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Set autoplay
|
|
538
|
+
*/
|
|
539
|
+
setAutoplay(autoplay = false): boolean {
|
|
540
|
+
if (typeof autoplay !== "boolean") {
|
|
541
|
+
throw this.#error("Autoplay must be a boolean value");
|
|
542
|
+
}
|
|
543
|
+
this.#autoplay = autoplay;
|
|
544
|
+
return this.#autoplay;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Set repeat mode
|
|
549
|
+
*/
|
|
550
|
+
setRepeatMode(repeatMode: RepeatMode = "none"): RepeatMode {
|
|
551
|
+
if (repeatMode !== "track" && repeatMode !== "queue" && repeatMode !== "none") {
|
|
552
|
+
throw this.#error("Repeat mode can only be set to track, queue, or none");
|
|
553
|
+
}
|
|
554
|
+
this.#repeatMode = repeatMode;
|
|
555
|
+
return this.#repeatMode;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Stop playback
|
|
560
|
+
*/
|
|
561
|
+
async stop(): Promise<void> {
|
|
562
|
+
return this.#update({ track: { encoded: null } });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Destroy the queue
|
|
567
|
+
*/
|
|
568
|
+
async destroy(reason?: string): Promise<void> {
|
|
569
|
+
return this.player.queues.destroy(this.guildId, reason);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Move a track from one position to another
|
|
574
|
+
* @param from - Current position of the track
|
|
575
|
+
* @param to - New position for the track
|
|
576
|
+
*/
|
|
577
|
+
move(from: number, to: number): Track | null {
|
|
578
|
+
if (from < 0 || from >= this.#tracks.length || to < 0 || to >= this.#tracks.length || from === to) {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const track = this.#tracks[from];
|
|
583
|
+
if (!track) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
this.#tracks.splice(from, 1);
|
|
588
|
+
this.#tracks.splice(to, 0, track);
|
|
589
|
+
|
|
590
|
+
return track;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Splice tracks - remove and/or add tracks at a specific position
|
|
595
|
+
* @param index - Position to start
|
|
596
|
+
* @param amount - Number of tracks to remove
|
|
597
|
+
* @param tracks - Tracks to add at the position
|
|
598
|
+
*/
|
|
599
|
+
splice(index: number, amount: number, tracks?: Track | Track[]): Track[] {
|
|
600
|
+
if (!this.#tracks.length && tracks) {
|
|
601
|
+
void this.add(tracks);
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const removed = tracks
|
|
606
|
+
? this.#tracks.splice(index, amount, ...(Array.isArray(tracks) ? tracks : [tracks]))
|
|
607
|
+
: this.#tracks.splice(index, amount);
|
|
608
|
+
|
|
609
|
+
return removed;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Sort tracks by a property or custom function
|
|
614
|
+
* @param sortBy - Property name or comparator function
|
|
615
|
+
* @param order - Sort order (asc/desc)
|
|
616
|
+
*/
|
|
617
|
+
sortBy(
|
|
618
|
+
sortBy: "duration" | "title" | "author" | ((a: Track, b: Track) => number),
|
|
619
|
+
order: "asc" | "desc" = "asc",
|
|
620
|
+
): this {
|
|
621
|
+
if (typeof sortBy === "function") {
|
|
622
|
+
this.#tracks.sort(sortBy);
|
|
623
|
+
} else {
|
|
624
|
+
this.#tracks.sort((a, b) => {
|
|
625
|
+
let comparison = 0;
|
|
626
|
+
|
|
627
|
+
switch (sortBy) {
|
|
628
|
+
case "duration":
|
|
629
|
+
comparison = a.duration - b.duration;
|
|
630
|
+
break;
|
|
631
|
+
case "title":
|
|
632
|
+
comparison = a.info.title.localeCompare(b.info.title);
|
|
633
|
+
break;
|
|
634
|
+
case "author":
|
|
635
|
+
comparison = a.info.author.localeCompare(b.info.author);
|
|
636
|
+
break;
|
|
637
|
+
default:
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return order === "desc" ? -comparison : comparison;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return this;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Get a sorted copy without modifying the original queue
|
|
650
|
+
*/
|
|
651
|
+
toSortedBy(
|
|
652
|
+
sortBy: "duration" | "title" | "author" | ((a: Track, b: Track) => number),
|
|
653
|
+
order: "asc" | "desc" = "asc",
|
|
654
|
+
): Track[] {
|
|
655
|
+
const copy = [...this.#tracks];
|
|
656
|
+
|
|
657
|
+
if (typeof sortBy === "function") {
|
|
658
|
+
return copy.sort(sortBy);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return copy.sort((a, b) => {
|
|
662
|
+
let comparison = 0;
|
|
663
|
+
|
|
664
|
+
switch (sortBy) {
|
|
665
|
+
case "duration":
|
|
666
|
+
comparison = a.duration - b.duration;
|
|
667
|
+
break;
|
|
668
|
+
case "title":
|
|
669
|
+
comparison = a.info.title.localeCompare(b.info.title);
|
|
670
|
+
break;
|
|
671
|
+
case "author":
|
|
672
|
+
comparison = a.info.author.localeCompare(b.info.author);
|
|
673
|
+
break;
|
|
674
|
+
default:
|
|
675
|
+
return 0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return order === "desc" ? -comparison : comparison;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Filter tracks by predicate or criteria
|
|
684
|
+
*/
|
|
685
|
+
filterTracks(
|
|
686
|
+
predicate:
|
|
687
|
+
| ((track: Track, index: number) => boolean)
|
|
688
|
+
| {
|
|
689
|
+
title?: string;
|
|
690
|
+
author?: string;
|
|
691
|
+
duration?: number | { min?: number; max?: number };
|
|
692
|
+
uri?: string;
|
|
693
|
+
identifier?: string;
|
|
694
|
+
sourceName?: string;
|
|
695
|
+
isStream?: boolean;
|
|
696
|
+
isSeekable?: boolean;
|
|
697
|
+
},
|
|
698
|
+
): Array<{ track: Track; index: number }> {
|
|
699
|
+
if (typeof predicate === "function") {
|
|
700
|
+
return this.#tracks
|
|
701
|
+
.map((track, index) => ({ track, index }))
|
|
702
|
+
.filter(({ track, index }) => predicate(track, index));
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return this.#tracks
|
|
706
|
+
.map((track, index) => ({ track, index }))
|
|
707
|
+
.filter(({ track }) => {
|
|
708
|
+
if (predicate.title && !track.info.title.toLowerCase().includes(predicate.title.toLowerCase())) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
if (predicate.author && !track.info.author.toLowerCase().includes(predicate.author.toLowerCase())) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
if (predicate.duration) {
|
|
715
|
+
if (typeof predicate.duration === "number") {
|
|
716
|
+
if (track.duration !== predicate.duration) {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
if (predicate.duration.min && track.duration < predicate.duration.min) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
if (predicate.duration.max && track.duration > predicate.duration.max) {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (predicate.uri && track.info.uri !== predicate.uri) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
if (predicate.identifier && track.info.identifier !== predicate.identifier) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
if (predicate.sourceName && track.info.sourceName !== predicate.sourceName) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
if (predicate.isStream !== undefined && track.isLive !== predicate.isStream) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
if (predicate.isSeekable !== undefined && track.isSeekable !== predicate.isSeekable) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return true;
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Find a track by predicate or criteria
|
|
750
|
+
*/
|
|
751
|
+
findTrack(
|
|
752
|
+
predicate:
|
|
753
|
+
| ((track: Track, index: number) => boolean)
|
|
754
|
+
| {
|
|
755
|
+
title?: string;
|
|
756
|
+
author?: string;
|
|
757
|
+
duration?: number | { min?: number; max?: number };
|
|
758
|
+
uri?: string;
|
|
759
|
+
identifier?: string;
|
|
760
|
+
sourceName?: string;
|
|
761
|
+
isStream?: boolean;
|
|
762
|
+
isSeekable?: boolean;
|
|
763
|
+
},
|
|
764
|
+
): { track: Track; index: number } | null {
|
|
765
|
+
const results = this.filterTracks(predicate);
|
|
766
|
+
return results[0] ?? null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Get a range of tracks
|
|
771
|
+
*/
|
|
772
|
+
getTracks(start: number, end?: number): Track[] {
|
|
773
|
+
return this.#tracks.slice(start, end);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Shift from previous tracks
|
|
778
|
+
*/
|
|
779
|
+
shiftPrevious(): Track | null {
|
|
780
|
+
return this.#previousTracks.shift() ?? null;
|
|
781
|
+
}
|
|
782
|
+
}
|