ryanlink 1.0.2 → 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 (62) hide show
  1. package/LICENSE +201 -37
  2. package/README.md +97 -370
  3. package/dist/index.cjs +4833 -0
  4. package/dist/index.d.cts +1703 -0
  5. package/dist/index.d.ts +1528 -1160
  6. package/dist/index.js +4262 -4197
  7. package/dist/index.mjs +4261 -4106
  8. package/package.json +87 -79
  9. package/dist/index.d.mts +0 -1335
  10. package/dist/index.js.map +0 -1
  11. package/dist/index.mjs.map +0 -1
  12. package/src/audio/AudioFilters.ts +0 -316
  13. package/src/audio/AudioQueue.ts +0 -782
  14. package/src/audio/AudioTrack.ts +0 -242
  15. package/src/audio/QueueController.ts +0 -252
  16. package/src/audio/TrackCollection.ts +0 -138
  17. package/src/audio/index.ts +0 -9
  18. package/src/config/defaults.ts +0 -223
  19. package/src/config/endpoints.ts +0 -99
  20. package/src/config/index.ts +0 -9
  21. package/src/config/patterns.ts +0 -55
  22. package/src/config/presets.ts +0 -400
  23. package/src/config/symbols.ts +0 -31
  24. package/src/core/PluginSystem.ts +0 -50
  25. package/src/core/RyanlinkPlayer.ts +0 -403
  26. package/src/core/index.ts +0 -6
  27. package/src/extensions/AutoplayExtension.ts +0 -283
  28. package/src/extensions/FairPlayExtension.ts +0 -154
  29. package/src/extensions/LyricsExtension.ts +0 -187
  30. package/src/extensions/PersistenceExtension.ts +0 -182
  31. package/src/extensions/SponsorBlockExtension.ts +0 -81
  32. package/src/extensions/index.ts +0 -9
  33. package/src/index.ts +0 -19
  34. package/src/lavalink/ConnectionPool.ts +0 -326
  35. package/src/lavalink/HttpClient.ts +0 -316
  36. package/src/lavalink/LavalinkConnection.ts +0 -409
  37. package/src/lavalink/index.ts +0 -7
  38. package/src/metadata.ts +0 -88
  39. package/src/types/api/Rest.ts +0 -949
  40. package/src/types/api/Websocket.ts +0 -463
  41. package/src/types/api/index.ts +0 -6
  42. package/src/types/audio/FilterManager.ts +0 -29
  43. package/src/types/audio/Queue.ts +0 -4
  44. package/src/types/audio/QueueManager.ts +0 -30
  45. package/src/types/audio/index.ts +0 -7
  46. package/src/types/common.ts +0 -63
  47. package/src/types/core/Player.ts +0 -322
  48. package/src/types/core/index.ts +0 -5
  49. package/src/types/index.ts +0 -6
  50. package/src/types/lavalink/Node.ts +0 -173
  51. package/src/types/lavalink/NodeManager.ts +0 -34
  52. package/src/types/lavalink/REST.ts +0 -144
  53. package/src/types/lavalink/index.ts +0 -32
  54. package/src/types/voice/VoiceManager.ts +0 -176
  55. package/src/types/voice/index.ts +0 -5
  56. package/src/utils/helpers.ts +0 -169
  57. package/src/utils/index.ts +0 -6
  58. package/src/utils/validators.ts +0 -184
  59. package/src/voice/RegionSelector.ts +0 -184
  60. package/src/voice/VoiceConnection.ts +0 -458
  61. package/src/voice/VoiceSession.ts +0 -297
  62. package/src/voice/index.ts +0 -7
@@ -1,184 +0,0 @@
1
- import { OnPingUpdateSymbol } from "../config/symbols";
2
- import type { PlayerState } from "../types";
3
- import type { Player } from "../core/RyanlinkPlayer";
4
-
5
- /**
6
- * Voice node ping statistics
7
- */
8
- interface VoiceNodePingStats {
9
- /**
10
- * History of ping measurements (up to 5 samples)
11
- */
12
- history: number[];
13
-
14
- /**
15
- * Timestamp of last ping measurement
16
- */
17
- lastPingTime: number;
18
- }
19
-
20
- /**
21
- * Represents a voice region with node ping tracking
22
- * Used for latency-based node selection
23
- */
24
- export class VoiceRegion {
25
- #pings = new Map<string, VoiceNodePingStats>();
26
-
27
- readonly id: string;
28
- readonly player: Player;
29
-
30
- constructor(player: Player, regionId: string) {
31
- if (player.voices.regions.has(regionId)) {
32
- throw new Error("An identical voice region already exists");
33
- }
34
-
35
- this.id = regionId;
36
- this.player = player;
37
-
38
- // Make properties immutable
39
- const immutable: PropertyDescriptor = {
40
- writable: false,
41
- configurable: false,
42
- };
43
-
44
- Object.defineProperties(this, {
45
- id: immutable,
46
- player: { ...immutable, enumerable: false },
47
- } as PropertyDescriptorMap);
48
- }
49
-
50
- /**
51
- * Check if all ready nodes have ping data for this region
52
- * @returns `true` if all nodes are synced, `false` otherwise
53
- */
54
- inSync(): boolean {
55
- return !Array.from(this.player.nodes.values()).some((n) => n.ready && !this.#pings.has(n.name));
56
- }
57
-
58
- /**
59
- * Remove ping data for a node
60
- * @param name Node name
61
- * @returns `true` if data was removed, `false` if it didn't exist
62
- */
63
- forgetNode(name: string): boolean {
64
- return this.#pings.delete(name);
65
- }
66
-
67
- /**
68
- * Get the average ping for a node in this region
69
- * @param name Node name
70
- * @returns Average ping in milliseconds, or `null` if no data
71
- */
72
- getAveragePing(name: string): number | null {
73
- const pings = this.#pings.get(name)?.history;
74
- if (!pings?.length) {
75
- return null;
76
- }
77
-
78
- return Math.round(pings.reduce((total, current) => total + current, 0) / pings.length);
79
- }
80
-
81
- /**
82
- * Get all nodes with their average pings
83
- * @returns Array of [nodeName, averagePing] tuples
84
- */
85
- getAllPings(): Array<[string, number | null]> {
86
- return Array.from(this.player.nodes.values()).map((node) => [node.name, this.getAveragePing(node.name)]);
87
- }
88
-
89
- /**
90
- * Get the most relevant node for this region
91
- * Selects based on lowest average ping
92
- * @returns The node with lowest ping, or first relevant node if no ping data
93
- */
94
- getRelevantNode() {
95
- return this.player.nodes.relevant().sort((a, b) => {
96
- const pingA = this.getAveragePing(a.name) ?? Number.MAX_SAFE_INTEGER;
97
- const pingB = this.getAveragePing(b.name) ?? Number.MAX_SAFE_INTEGER;
98
- return pingA - pingB;
99
- })[0];
100
- }
101
-
102
- /**
103
- * Internal method to update ping statistics
104
- * Called by the voice manager when player state updates
105
- * @param name Node name
106
- * @param state Player state with ping information
107
- * @internal
108
- */
109
- [OnPingUpdateSymbol](name: string, state: PlayerState): void {
110
- // Only process if connected and has valid ping data
111
- if (!state.connected) {
112
- return;
113
- }
114
- if (state.ping <= 0 || state.time <= 0) {
115
- return;
116
- }
117
-
118
- const pings = this.#pings.get(name);
119
-
120
- // Initialize ping tracking for this node
121
- if (!pings) {
122
- this.#pings.set(name, {
123
- history: [state.ping],
124
- lastPingTime: state.time,
125
- });
126
- return;
127
- }
128
-
129
- // Only update if enough time has passed (12 seconds)
130
- if (state.time - pings.lastPingTime < 12_000) {
131
- return;
132
- }
133
-
134
- pings.lastPingTime = state.time;
135
- pings.history.push(state.ping);
136
-
137
- // Keep only last 5 samples
138
- if (pings.history.length > 5) {
139
- pings.history.shift();
140
- }
141
- }
142
-
143
- /**
144
- * Clear all ping data for this region
145
- */
146
- clear(): void {
147
- this.#pings.clear();
148
- }
149
-
150
- /**
151
- * Get the number of nodes with ping data
152
- */
153
- get nodeCount(): number {
154
- return this.#pings.size;
155
- }
156
-
157
- /**
158
- * String representation of the voice region
159
- */
160
- toString(): string {
161
- return `VoiceRegion<${this.id}>`;
162
- }
163
-
164
- /**
165
- * JSON representation of the voice region
166
- */
167
- toJSON(): {
168
- id: string;
169
- inSync: boolean;
170
- nodeCount: number;
171
- pings: Array<{ node: string; averagePing: number | null; samples: number }>;
172
- } {
173
- return {
174
- id: this.id,
175
- inSync: this.inSync(),
176
- nodeCount: this.nodeCount,
177
- pings: Array.from(this.#pings.entries()).map(([node, stats]) => ({
178
- node,
179
- averagePing: this.getAveragePing(node),
180
- samples: stats.history.length,
181
- })),
182
- };
183
- }
184
- }
@@ -1,458 +0,0 @@
1
- import { setTimeout } from "node:timers/promises";
2
- import { SnowflakeRegex, VoiceRegionIdRegex } from "../config";
3
- import { LookupSymbol, OnVoiceCloseSymbol, UpdateSymbol } from "../config/symbols";
4
- import { isString, noop } from "../utils";
5
- import { VoiceRegion } from "./RegionSelector";
6
- import { VoiceState } from "./VoiceSession";
7
- import {
8
- VoiceCloseCodes,
9
- type BotReadyPayload,
10
- type BotVoiceState,
11
- type ConnectOptions,
12
- type CreateQueueOptions,
13
- type DiscordDispatchPayload,
14
- type VoiceServerUpdatePayload,
15
- type VoiceStateUpdatePayload,
16
- } from "../types";
17
- import type { Player } from "../core/RyanlinkPlayer";
18
-
19
- /**
20
- * Join request with promise resolvers
21
- */
22
- interface JoinRequest extends Pick<CreateQueueOptions, "context" | "node" | "voiceId"> {
23
- promise: Promise<VoiceState>;
24
- resolve: (value: VoiceState | PromiseLike<VoiceState>) => void;
25
- reject: (reason?: unknown) => void;
26
- config?: Pick<CreateQueueOptions, "filters" | "volume">;
27
- }
28
-
29
- /**
30
- * Manages voice connections for all guilds
31
- * Handles Discord voice events and Lavalink voice state
32
- */
33
- export class VoiceManager implements Partial<Map<string, VoiceState>> {
34
- #cache = new Map<string, BotVoiceState>();
35
- #voices = new Map<string, VoiceState>();
36
-
37
- #joins = new Map<string, JoinRequest>();
38
- #destroys = new Map<string, Promise<void>>();
39
-
40
- readonly regions = new Map<string, VoiceRegion>();
41
- readonly player: Player;
42
-
43
- constructor(player: Player) {
44
- if (player.voices === undefined) {
45
- this.player = player;
46
- } else {
47
- throw new Error("Manager already exists for this Player");
48
- }
49
-
50
- const immutable: PropertyDescriptor = {
51
- writable: false,
52
- configurable: false,
53
- };
54
-
55
- Object.defineProperties(this, {
56
- regions: immutable,
57
- player: { ...immutable, enumerable: false },
58
- } as PropertyDescriptorMap);
59
- }
60
-
61
- /**
62
- * Number of voice connections
63
- */
64
- get size(): number {
65
- return this.#voices.size;
66
- }
67
-
68
- /**
69
- * Get voice state for a guild
70
- */
71
- get(guildId: string): VoiceState | undefined {
72
- return this.#voices.get(guildId);
73
- }
74
-
75
- /**
76
- * Check if voice state exists
77
- */
78
- has(guildId: string): boolean {
79
- return this.#voices.has(guildId);
80
- }
81
-
82
- /**
83
- * Get all connected guild IDs
84
- */
85
- keys() {
86
- return this.#voices.keys();
87
- }
88
-
89
- /**
90
- * Get all voice connections
91
- */
92
- values() {
93
- return this.#voices.values();
94
- }
95
-
96
- /**
97
- * Get all voice connections as entries
98
- */
99
- entries() {
100
- return this.#voices.entries();
101
- }
102
-
103
- /**
104
- * Destroy a voice connection
105
- * @param guildId Guild ID
106
- * @param reason Reason for destruction
107
- */
108
- async destroy(guildId: string, reason = "destroyed"): Promise<void> {
109
- if (this.player.queues.has(guildId)) {
110
- return this.player.queues.destroy(guildId, reason);
111
- }
112
- if (this.#destroys.has(guildId)) {
113
- return this.#destroys.get(guildId) ?? Promise.resolve();
114
- }
115
-
116
- const voice = this.#voices.get(guildId);
117
- if (!voice) {
118
- return;
119
- }
120
-
121
- let resolve!: () => void;
122
- let reject!: (reason?: unknown) => void;
123
- const promise = new Promise<void>((res, rej) => {
124
- resolve = res;
125
- reject = rej;
126
- });
127
- const resolver = { promise, resolve, reject };
128
- this.#destroys.set(guildId, resolver.promise);
129
-
130
- if (this[LookupSymbol](guildId)?.connected) {
131
- await voice.disconnect().catch(noop);
132
- }
133
-
134
- this.#cache.delete(guildId);
135
- this.#voices.delete(guildId);
136
-
137
- this.player.emit("voiceDestroy", voice, reason);
138
-
139
- resolver.resolve();
140
- this.#destroys.delete(guildId);
141
- }
142
-
143
- /**
144
- * Connect to a voice channel
145
- * @param guildId Guild ID
146
- * @param voiceId Voice channel ID
147
- * @param options Connection options
148
- */
149
- async connect(guildId: string, voiceId: string, options?: ConnectOptions): Promise<VoiceState> {
150
- if (!isString(guildId, SnowflakeRegex)) {
151
- throw new Error("Guild Id is not a valid Discord Id");
152
- }
153
- if (!isString(voiceId, SnowflakeRegex)) {
154
- throw new Error("Voice Id is not a valid Discord Id");
155
- }
156
-
157
- const currentRequest = this.#joins.get(guildId);
158
- if (currentRequest) {
159
- if (currentRequest.voiceId === voiceId) {
160
- return currentRequest.promise;
161
- }
162
- currentRequest.reject(new Error("Connection request was replaced"));
163
- }
164
-
165
- const state = this.#cache.get(guildId);
166
- if (state?.connected && state.channel_id === voiceId) {
167
- const voice = this.player.queues.get(guildId)?.voice;
168
- if (voice && voice.connected) {
169
- return voice;
170
- }
171
- }
172
-
173
- this.#joins.delete(guildId);
174
-
175
- let resolve!: (value: VoiceState | PromiseLike<VoiceState>) => void;
176
- let reject!: (reason?: unknown) => void;
177
- const promise = new Promise<VoiceState>((res, rej) => {
178
- resolve = res;
179
- reject = rej;
180
- });
181
- const request: JoinRequest = {
182
- promise,
183
- resolve,
184
- reject,
185
- voiceId,
186
- node: options?.node,
187
- context: options?.context,
188
- config: options ? { filters: options.filters, volume: options.volume } : undefined,
189
- };
190
-
191
- this.#joins.set(guildId, request);
192
-
193
- // Send voice state update to Discord
194
- await this.player.options.forwardVoiceUpdate(guildId, {
195
- op: 4,
196
- d: {
197
- guild_id: guildId,
198
- channel_id: voiceId,
199
- self_deaf: false,
200
- self_mute: false,
201
- },
202
- });
203
-
204
- // Wait for voice connection with timeout
205
- const timeout = setTimeout(15_000, undefined, { ref: false });
206
- const result = await Promise.race([request.promise, timeout]);
207
-
208
- if (result === undefined) {
209
- this.#joins.delete(guildId);
210
- throw new Error("Voice connection timed out");
211
- }
212
-
213
- return result;
214
- }
215
-
216
- /**
217
- * Disconnect from voice channel
218
- * @param guildId Guild ID
219
- */
220
- async disconnect(guildId: string): Promise<void> {
221
- const state = this.#cache.get(guildId);
222
- if (!state) {
223
- return;
224
- }
225
-
226
- await this.player.options.forwardVoiceUpdate(guildId, {
227
- op: 4,
228
- d: {
229
- guild_id: guildId,
230
- channel_id: null,
231
- self_deaf: false,
232
- self_mute: false,
233
- },
234
- });
235
-
236
- state.channel_id = "";
237
- state.connected = false;
238
- }
239
-
240
- /**
241
- * Handle Discord dispatch events
242
- * @param payload Discord gateway payload
243
- */
244
- handleDispatch(payload: DiscordDispatchPayload): void {
245
- if (payload.t === "READY") {
246
- this.#handleReady(payload);
247
- } else if (payload.t === "VOICE_STATE_UPDATE") {
248
- this.#handleVoiceStateUpdate(payload);
249
- } else if (payload.t === "VOICE_SERVER_UPDATE") {
250
- this.#handleVoiceServerUpdate(payload);
251
- }
252
- }
253
-
254
- /**
255
- * Internal lookup for voice state
256
- * @internal
257
- */
258
- [LookupSymbol](guildId: string): BotVoiceState | undefined {
259
- return this.#cache.get(guildId);
260
- }
261
-
262
- /**
263
- * Internal update for voice state
264
- * @internal
265
- */
266
- [UpdateSymbol](guildId: string, partial: Partial<BotVoiceState>): void {
267
- const state = this.#cache.get(guildId);
268
- if (state) {
269
- Object.assign(state, partial);
270
- }
271
- }
272
-
273
- /**
274
- * Internal voice close handler
275
- * @internal
276
- */
277
- [OnVoiceCloseSymbol](guildId: string, code: number, reason: string, byRemote: boolean): void {
278
- const voice = this.#voices.get(guildId);
279
- if (!voice) {
280
- return;
281
- }
282
-
283
- this.player.emit("voiceClose", voice, code, reason, byRemote);
284
-
285
- // Check if we should destroy the connection
286
- const shouldDestroy =
287
- (code as VoiceCloseCodes) === VoiceCloseCodes.Disconnected ||
288
- (code as VoiceCloseCodes) === VoiceCloseCodes.DisconnectedRateLimited ||
289
- (code as VoiceCloseCodes) === VoiceCloseCodes.DisconnectedCallTerminated;
290
-
291
- if (shouldDestroy) {
292
- this.destroy(guildId, `Voice closed: ${reason} (${code})`).catch(noop);
293
- }
294
- }
295
-
296
- /**
297
- * Handle READY event
298
- */
299
- #handleReady(_payload: BotReadyPayload): void {
300
- // Clear all voice states on ready
301
- this.#cache.clear();
302
- this.#voices.clear();
303
- this.#joins.clear();
304
- }
305
-
306
- /**
307
- * Handle VOICE_STATE_UPDATE event
308
- */
309
- #handleVoiceStateUpdate(payload: VoiceStateUpdatePayload): void {
310
- const { d: data } = payload;
311
- const guildId = data.guild_id;
312
-
313
- if (!guildId || data.user_id !== this.player.clientId) {
314
- return;
315
- }
316
-
317
- let state = this.#cache.get(guildId);
318
-
319
- // User left voice channel
320
- if (data.channel_id === null) {
321
- if (state) {
322
- state.channel_id = "";
323
- state.connected = false;
324
- }
325
- return;
326
- }
327
-
328
- // Create or update state
329
- if (!state) {
330
- state = {
331
- channel_id: data.channel_id,
332
- session_id: data.session_id,
333
- deaf: data.deaf,
334
- mute: data.mute,
335
- self_deaf: data.self_deaf,
336
- self_mute: data.self_mute,
337
- suppress: data.suppress,
338
- token: "",
339
- endpoint: "",
340
- connected: false,
341
- node_session_id: "",
342
- reconnecting: false,
343
- region_id: "",
344
- };
345
- this.#cache.set(guildId, state);
346
- } else {
347
- state.channel_id = data.channel_id;
348
- state.session_id = data.session_id;
349
- state.deaf = data.deaf;
350
- state.mute = data.mute;
351
- state.self_deaf = data.self_deaf;
352
- state.self_mute = data.self_mute;
353
- state.suppress = data.suppress;
354
- }
355
-
356
- void this.#tryConnect(guildId);
357
- }
358
-
359
- /**
360
- * Handle VOICE_SERVER_UPDATE event
361
- */
362
- #handleVoiceServerUpdate(payload: VoiceServerUpdatePayload): void {
363
- const { d: data } = payload;
364
- const guildId = data.guild_id;
365
-
366
- let state = this.#cache.get(guildId);
367
-
368
- if (!state) {
369
- state = {
370
- channel_id: "",
371
- session_id: "",
372
- deaf: false,
373
- mute: false,
374
- self_deaf: false,
375
- self_mute: false,
376
- suppress: false,
377
- token: data.token,
378
- endpoint: data.endpoint ?? "",
379
- connected: false,
380
- node_session_id: "",
381
- reconnecting: false,
382
- region_id: "",
383
- };
384
- this.#cache.set(guildId, state);
385
- } else {
386
- state.token = data.token;
387
- state.endpoint = data.endpoint ?? "";
388
- }
389
-
390
- // Extract region from endpoint
391
- if (state.endpoint) {
392
- const match = state.endpoint.match(VoiceRegionIdRegex);
393
- if (match?.[1]) {
394
- state.region_id = match[1];
395
-
396
- // Create or get voice region
397
- if (!this.regions.has(state.region_id)) {
398
- this.regions.set(state.region_id, new VoiceRegion(this.player, state.region_id));
399
- }
400
- }
401
- }
402
-
403
- void this.#tryConnect(guildId);
404
- }
405
-
406
- /**
407
- * Try to establish voice connection
408
- */
409
- async #tryConnect(guildId: string): Promise<void> {
410
- const state = this.#cache.get(guildId);
411
- const request = this.#joins.get(guildId);
412
-
413
- if (!state || !request) {
414
- return;
415
- }
416
-
417
- if (!state.channel_id || !state.session_id || !state.token || !state.endpoint) {
418
- return;
419
- }
420
-
421
- this.#joins.delete(guildId);
422
-
423
- try {
424
- const nodeName =
425
- request.node ??
426
- (state.region_id && this.regions.get(state.region_id)?.getRelevantNode()?.name) ??
427
- this.player.nodes.relevant()[0]?.name;
428
-
429
- if (!nodeName) {
430
- throw new Error("No nodes available");
431
- }
432
-
433
- let queue = this.player.queues.get(guildId);
434
- if (!queue) {
435
- queue = await this.player.queues.create({
436
- guildId,
437
- voiceId: state.channel_id,
438
- node: nodeName,
439
- context: request.context,
440
- ...request.config,
441
- });
442
- }
443
-
444
- const voice = new VoiceState(this.player, nodeName, guildId);
445
- this.#voices.set(guildId, voice);
446
-
447
- state.connected = true;
448
- state.node_session_id = voice.node.sessionId ?? "";
449
-
450
- await queue.sync("remote").catch(noop);
451
-
452
- this.player.emit("voiceConnect", voice);
453
- request.resolve(voice);
454
- } catch (error) {
455
- request.reject(error instanceof Error ? error : new Error(String(error)));
456
- }
457
- }
458
- }