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.
Files changed (60) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +455 -0
  3. package/dist/index.d.mts +1335 -0
  4. package/dist/index.d.ts +1335 -0
  5. package/dist/index.js +4694 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +4604 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +82 -0
  10. package/src/audio/AudioFilters.ts +316 -0
  11. package/src/audio/AudioQueue.ts +782 -0
  12. package/src/audio/AudioTrack.ts +242 -0
  13. package/src/audio/QueueController.ts +252 -0
  14. package/src/audio/TrackCollection.ts +138 -0
  15. package/src/audio/index.ts +9 -0
  16. package/src/config/defaults.ts +223 -0
  17. package/src/config/endpoints.ts +99 -0
  18. package/src/config/index.ts +9 -0
  19. package/src/config/patterns.ts +55 -0
  20. package/src/config/presets.ts +400 -0
  21. package/src/config/symbols.ts +31 -0
  22. package/src/core/PluginSystem.ts +50 -0
  23. package/src/core/RyanlinkPlayer.ts +403 -0
  24. package/src/core/index.ts +6 -0
  25. package/src/extensions/AutoplayExtension.ts +283 -0
  26. package/src/extensions/FairPlayExtension.ts +154 -0
  27. package/src/extensions/LyricsExtension.ts +187 -0
  28. package/src/extensions/PersistenceExtension.ts +182 -0
  29. package/src/extensions/SponsorBlockExtension.ts +81 -0
  30. package/src/extensions/index.ts +9 -0
  31. package/src/index.ts +19 -0
  32. package/src/lavalink/ConnectionPool.ts +326 -0
  33. package/src/lavalink/HttpClient.ts +316 -0
  34. package/src/lavalink/LavalinkConnection.ts +409 -0
  35. package/src/lavalink/index.ts +7 -0
  36. package/src/metadata.ts +88 -0
  37. package/src/types/api/Rest.ts +949 -0
  38. package/src/types/api/Websocket.ts +463 -0
  39. package/src/types/api/index.ts +6 -0
  40. package/src/types/audio/FilterManager.ts +29 -0
  41. package/src/types/audio/Queue.ts +4 -0
  42. package/src/types/audio/QueueManager.ts +30 -0
  43. package/src/types/audio/index.ts +7 -0
  44. package/src/types/common.ts +63 -0
  45. package/src/types/core/Player.ts +322 -0
  46. package/src/types/core/index.ts +5 -0
  47. package/src/types/index.ts +6 -0
  48. package/src/types/lavalink/Node.ts +173 -0
  49. package/src/types/lavalink/NodeManager.ts +34 -0
  50. package/src/types/lavalink/REST.ts +144 -0
  51. package/src/types/lavalink/index.ts +32 -0
  52. package/src/types/voice/VoiceManager.ts +176 -0
  53. package/src/types/voice/index.ts +5 -0
  54. package/src/utils/helpers.ts +169 -0
  55. package/src/utils/index.ts +6 -0
  56. package/src/utils/validators.ts +184 -0
  57. package/src/voice/RegionSelector.ts +184 -0
  58. package/src/voice/VoiceConnection.ts +451 -0
  59. package/src/voice/VoiceSession.ts +297 -0
  60. package/src/voice/index.ts +7 -0
@@ -0,0 +1,409 @@
1
+ import { EventEmitter, once } from "node:events";
2
+ import { Buffer } from "node:buffer";
3
+ import { clearTimeout, setTimeout } from "node:timers";
4
+ import { WebSocket, type ClientOptions } from "ws";
5
+ import { REST } from "./HttpClient";
6
+ import { CLIENT_NAME, CLIENT_VERSION } from "../metadata";
7
+ import { OPType } from "../types/api/Websocket";
8
+ import type {
9
+ NodeOptions,
10
+ NodeState,
11
+ NodeEventMap,
12
+ MessagePayload,
13
+ StatsPayload,
14
+ ClientHeaders,
15
+ } from "../types/lavalink";
16
+
17
+ export enum CloseCodes {
18
+ Normal = 1000,
19
+ GoingAway = 1001,
20
+ ProtocolError = 1002,
21
+ UnsupportedData = 1003,
22
+ NoStatusReceived = 1005,
23
+ AbnormalClosure = 1006,
24
+ InvalidFramePayloadData = 1007,
25
+ PolicyViolation = 1008,
26
+ MessageTooBig = 1009,
27
+ InternalError = 1011,
28
+ }
29
+
30
+ /**
31
+ * Represents a Lavalink node connection
32
+ * Handles WebSocket connection, reconnection, and session management
33
+ */
34
+ export class LavalinkNode extends EventEmitter<NodeEventMap> {
35
+ #socketConfig: ClientOptions & { headers: ClientHeaders };
36
+ #connectPromise: Promise<boolean> | null = null;
37
+ #disconnectPromise: Promise<void> | null = null;
38
+
39
+ #pingTimer: NodeJS.Timeout | null = null;
40
+ #reconnectTimer: NodeJS.Timeout | null = null;
41
+
42
+ #ping: number | null = null;
43
+ #lastPingTime: number | null = null;
44
+
45
+ #reconnectCycle = true;
46
+ #reconnectAttempts = 0;
47
+ #manualDisconnect = false;
48
+
49
+ #socket: WebSocket | null = null;
50
+ #stats: StatsPayload | null = null;
51
+
52
+ #socketUrl: string;
53
+ #pingTimeout: number;
54
+ #reconnectDelay: number;
55
+ #reconnectLimit: number;
56
+
57
+ readonly name: string;
58
+ readonly rest: REST;
59
+
60
+ constructor(options: NodeOptions) {
61
+ super({ captureRejections: false });
62
+
63
+ // Validate options
64
+ if (!options.name || typeof options.name !== "string") {
65
+ throw new Error("Node name must be a non-empty string");
66
+ }
67
+
68
+ if (!options.clientId || typeof options.clientId !== "string") {
69
+ throw new Error("Client ID must be a non-empty string");
70
+ }
71
+
72
+ // Initialize REST client
73
+ this.rest = new REST(options);
74
+
75
+ // Setup socket configuration
76
+ this.#socketConfig = {
77
+ headers: {
78
+ "Client-Name": `${CLIENT_NAME}/${CLIENT_VERSION}`,
79
+ "User-Id": options.clientId,
80
+ "User-Agent": this.rest.userAgent,
81
+ Authorization: options.password,
82
+ },
83
+ perMessageDeflate: false,
84
+ handshakeTimeout: options.handshakeTimeout ?? 30000,
85
+ };
86
+
87
+ // Restore session if provided
88
+ if (this.rest.sessionId) {
89
+ this.#socketConfig.headers["Session-Id"] = this.rest.sessionId;
90
+ }
91
+
92
+ // Build WebSocket URL
93
+ const protocol = options.secure ? "wss" : "ws";
94
+ this.#socketUrl = `${protocol}://${options.host}:${options.port}/v4/websocket`;
95
+
96
+ // Configure timeouts and reconnection
97
+ this.#pingTimeout = (options.statsInterval ?? 60000) + (options.highestLatency ?? 5000);
98
+ this.#reconnectDelay = options.reconnectDelay ?? 5000;
99
+ this.#reconnectLimit = options.reconnectLimit ?? -1; // -1 = infinite
100
+
101
+ this.name = options.name;
102
+
103
+ // Make REST sessionId readonly but linked to socket
104
+ Object.defineProperty(this.rest, "sessionId", {
105
+ configurable: false,
106
+ get: () => this.sessionId,
107
+ set: () => {},
108
+ });
109
+
110
+ // Make properties immutable
111
+ const immutable: PropertyDescriptor = {
112
+ writable: false,
113
+ configurable: false,
114
+ };
115
+
116
+ Object.defineProperties(this, {
117
+ name: immutable,
118
+ rest: immutable,
119
+ } as PropertyDescriptorMap);
120
+ }
121
+
122
+ get clientId(): string {
123
+ return this.#socketConfig.headers["User-Id"];
124
+ }
125
+
126
+ get sessionId(): string | null {
127
+ return this.#socketConfig.headers["Session-Id"] ?? null;
128
+ }
129
+
130
+ get ping(): number | null {
131
+ return this.#ping;
132
+ }
133
+
134
+ get stats(): StatsPayload | null {
135
+ return this.#stats;
136
+ }
137
+
138
+ get state(): NodeState {
139
+ if (this.connecting) {
140
+ return "connecting";
141
+ }
142
+ if (this.connected) {
143
+ return this.ready ? "ready" : "connected";
144
+ }
145
+ return this.reconnecting ? "reconnecting" : "disconnected";
146
+ }
147
+
148
+ get connecting(): boolean {
149
+ return this.#socket?.readyState === WebSocket.CONNECTING;
150
+ }
151
+
152
+ get connected(): boolean {
153
+ return this.#socket?.readyState === WebSocket.OPEN;
154
+ }
155
+
156
+ get ready(): boolean {
157
+ return this.connected && this.sessionId !== null;
158
+ }
159
+
160
+ get reconnecting(): boolean {
161
+ return this.#socket === null && this.#reconnectTimer !== null;
162
+ }
163
+
164
+ get disconnected(): boolean {
165
+ return this.#socket === null && !this.reconnecting;
166
+ }
167
+
168
+ get reconnectLimit(): number {
169
+ return this.#reconnectLimit;
170
+ }
171
+
172
+ get reconnectAttempts(): number {
173
+ return this.#reconnectAttempts;
174
+ }
175
+
176
+ #error(err: Error | (Error & { errors?: Error[] })): Error {
177
+ const data = "errors" in err && Array.isArray(err.errors) ? err.errors[err.errors.length - 1] : err;
178
+ const error = data instanceof Error ? data : new Error(String(data));
179
+ error.name = `Error [${(this.constructor as { name: string }).name}]`;
180
+ return error;
181
+ }
182
+
183
+ #cleanup(): void {
184
+ this.#socket?.removeAllListeners();
185
+ if (this.#pingTimer !== null) {
186
+ clearTimeout(this.#pingTimer);
187
+ }
188
+ this.#socket = this.#pingTimer = this.#stats = null;
189
+ this.#lastPingTime = this.#ping = null;
190
+ }
191
+
192
+ #reconnect(): void {
193
+ this.#reconnectCycle = false;
194
+ this.#reconnectTimer?.refresh();
195
+ this.#reconnectTimer ??= setTimeout(() => {
196
+ this.#reconnectCycle = true;
197
+ void this.connect();
198
+ }, this.#reconnectDelay).unref();
199
+ }
200
+
201
+ #stopReconnecting(resetCount = true, reconnectCycle = false): void {
202
+ this.#reconnectCycle = reconnectCycle;
203
+ if (resetCount) {
204
+ this.#reconnectAttempts = 0;
205
+ }
206
+ if (this.#reconnectTimer !== null) {
207
+ clearTimeout(this.#reconnectTimer);
208
+ }
209
+ this.#reconnectTimer = null;
210
+ }
211
+
212
+ #keepAliveAndPing(): void {
213
+ this.#pingTimer?.refresh();
214
+ this.#pingTimer ??= setTimeout(() => {
215
+ this.#socket?.terminate();
216
+ this.#cleanup();
217
+ this.#reconnect();
218
+ }, this.#pingTimeout).unref();
219
+
220
+ // Record timestamp before sending ping
221
+ this.#lastPingTime = Date.now();
222
+ this.#socket?.ping();
223
+ }
224
+
225
+ #parseMessageData(data: string): MessagePayload | null {
226
+ try {
227
+ return JSON.parse(data) as MessagePayload;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Connect to the Lavalink node
235
+ * Handles reconnection attempts and session resumption
236
+ */
237
+ async connect(): Promise<boolean> {
238
+ if (this.#socket !== null) {
239
+ return this.#connectPromise ?? this.connected;
240
+ }
241
+
242
+ if (this.reconnecting) {
243
+ this.#reconnectAttempts++;
244
+ if (!this.#reconnectCycle) {
245
+ this.#stopReconnecting(false, true);
246
+ }
247
+ }
248
+
249
+ this.#socket = new WebSocket(this.#socketUrl, this.#socketConfig);
250
+
251
+ this.#socket.once("open", () => {
252
+ this.emit("connect", this.#reconnectAttempts, this.name);
253
+ });
254
+
255
+ this.#socket.on("message", (data) => {
256
+ void this.#onMessage((data as Buffer).toString("utf8"));
257
+ });
258
+
259
+ this.#socket.on("error", (err) => {
260
+ this.emit("error", this.#error(err), this.name);
261
+ });
262
+
263
+ this.#socket.on("close", (code, reason) => {
264
+ this.#onClose(code, reason.toString("utf8"));
265
+ });
266
+
267
+ this.#socket.on("pong", () => {
268
+ if (this.#lastPingTime === null) {
269
+ return;
270
+ }
271
+ this.#ping = Math.max(0, Date.now() - this.#lastPingTime);
272
+ });
273
+
274
+ let resolve!: (value: boolean) => void;
275
+ let reject!: (reason?: unknown) => void;
276
+ const promise = new Promise<boolean>((res, rej) => {
277
+ resolve = res;
278
+ reject = rej;
279
+ });
280
+ const resolver = { promise, resolve, reject };
281
+ this.#connectPromise = resolver.promise;
282
+ const controller = new AbortController();
283
+
284
+ try {
285
+ await Promise.race([
286
+ once(this.#socket, "open", { signal: controller.signal }),
287
+ once(this.#socket, "close", { signal: controller.signal }),
288
+ ]);
289
+ } catch {
290
+ this.#cleanup();
291
+ } finally {
292
+ controller.abort();
293
+ const connected = this.connected;
294
+ resolver.resolve(connected);
295
+ this.#connectPromise = null;
296
+ }
297
+ return this.connected;
298
+ }
299
+
300
+ /**
301
+ * Disconnect from the Lavalink node
302
+ * @param code - WebSocket close code
303
+ * @param reason - Disconnect reason
304
+ */
305
+ async disconnect(code: number = CloseCodes.Normal, reason = "disconnected"): Promise<void> {
306
+ if (this.#disconnectPromise !== null) {
307
+ return this.#disconnectPromise;
308
+ }
309
+
310
+ this.#stopReconnecting();
311
+
312
+ if (this.#socket === null) {
313
+ return;
314
+ }
315
+
316
+ if (this.connecting) {
317
+ this.#manualDisconnect = true;
318
+ this.#socket.terminate();
319
+ return;
320
+ }
321
+
322
+ if (!this.connected) {
323
+ return;
324
+ }
325
+
326
+ this.#manualDisconnect = true;
327
+ this.#disconnectPromise = once(this.#socket, "close").then(
328
+ () => {},
329
+ () => {},
330
+ );
331
+ this.#socket.close(code, reason);
332
+ await this.#disconnectPromise;
333
+ this.#disconnectPromise = null;
334
+ }
335
+
336
+ async #onMessage(data: string): Promise<void> {
337
+ const payload = this.#parseMessageData(data);
338
+ if (payload === null) {
339
+ return this.disconnect(CloseCodes.UnsupportedData, "expected json payload");
340
+ }
341
+
342
+ if (payload.op === OPType.Stats) {
343
+ this.#stats = payload;
344
+ this.#keepAliveAndPing();
345
+ } else if (payload.op === OPType.Ready) {
346
+ this.#stopReconnecting();
347
+ this.#socketConfig.headers["Session-Id"] = payload.sessionId;
348
+ this.emit("ready", payload.resumed, payload.sessionId, this.name);
349
+ }
350
+
351
+ this.emit("dispatch", payload, this.name);
352
+ }
353
+
354
+ #onClose(code: number, reason: string): void {
355
+ this.#cleanup();
356
+
357
+ // Check if we should stop reconnecting
358
+ const shouldStop =
359
+ this.#manualDisconnect || (this.#reconnectLimit >= 0 && this.#reconnectAttempts >= this.#reconnectLimit);
360
+
361
+ if (shouldStop) {
362
+ this.#stopReconnecting();
363
+ delete this.#socketConfig.headers["Session-Id"];
364
+ const byLocal = this.#manualDisconnect;
365
+ this.#manualDisconnect = false;
366
+ this.emit("disconnect", code, reason, byLocal, this.name);
367
+ return;
368
+ }
369
+
370
+ if (this.#reconnectCycle) {
371
+ this.#reconnect();
372
+ this.emit("close", code, reason, this.name);
373
+ return;
374
+ }
375
+
376
+ // Immediate reconnect attempt
377
+ setTimeout(() => {
378
+ this.#reconnectCycle = true;
379
+ void this.connect();
380
+ }, 0);
381
+ }
382
+
383
+ /**
384
+ * Set SponsorBlock segments for a player
385
+ */
386
+ async setSponsorBlock(player: { guildId: string }, segments: string[]): Promise<void> {
387
+ return this.rest.setSponsorBlock(player.guildId, segments);
388
+ }
389
+
390
+ /**
391
+ * Get current SponsorBlock segments for a player
392
+ */
393
+ async getSponsorBlock(player: { guildId: string }): Promise<string[]> {
394
+ return this.rest.getSponsorBlock(player.guildId);
395
+ }
396
+
397
+ /**
398
+ * Delete SponsorBlock configuration for a player
399
+ */
400
+ async deleteSponsorBlock(player: { guildId: string }): Promise<void> {
401
+ return this.rest.deleteSponsorBlock(player.guildId);
402
+ }
403
+
404
+ override toString(): string {
405
+ return this.name;
406
+ }
407
+ }
408
+
409
+ export { LavalinkNode as Node };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Lavalink exports - HttpClient, LavalinkConnection, ConnectionPool
3
+ */
4
+
5
+ export * from "./HttpClient";
6
+ export * from "./LavalinkConnection";
7
+ export * from "./ConnectionPool";
@@ -0,0 +1,88 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ interface PackageJson {
5
+ name: string;
6
+ version: string;
7
+ description?: string;
8
+ repository?: {
9
+ type: string;
10
+ url: string;
11
+ };
12
+ }
13
+
14
+ let packageInfo: PackageJson | null = null;
15
+
16
+ /**
17
+ * Load package.json information
18
+ * Cached after first load for performance
19
+ */
20
+ function loadPackageInfo(): PackageJson {
21
+ if (packageInfo) {
22
+ return packageInfo;
23
+ }
24
+
25
+ try {
26
+ const possiblePaths = [
27
+ join(__dirname, "..", "..", "package.json"),
28
+ join(__dirname, "..", "package.json"),
29
+ join(process.cwd(), "package.json"),
30
+ ];
31
+
32
+ for (const path of possiblePaths) {
33
+ try {
34
+ const content = readFileSync(path, "utf-8");
35
+ packageInfo = JSON.parse(content) as PackageJson;
36
+ return packageInfo;
37
+ } catch {
38
+ continue;
39
+ }
40
+ }
41
+
42
+ console.warn("Could not load package.json, using fallback values");
43
+ return {
44
+ name: "ryanlink",
45
+ version: "1.0.0",
46
+ repository: {
47
+ type: "git",
48
+ url: "https://github.com/ryanwtf7/ryanlink.git",
49
+ },
50
+ };
51
+ } catch (error) {
52
+ return {
53
+ name: "ryanlink",
54
+ version: "1.0.0",
55
+ repository: {
56
+ type: "git",
57
+ url: "https://github.com/ryanwtf7/ryanlink.git",
58
+ },
59
+ };
60
+ }
61
+ }
62
+
63
+ const pkg = loadPackageInfo();
64
+
65
+ /**
66
+ * Package name
67
+ */
68
+ export const CLIENT_NAME = pkg.name;
69
+
70
+ /**
71
+ * Package version
72
+ */
73
+ export const CLIENT_VERSION = pkg.version;
74
+
75
+ /**
76
+ * Package repository URL
77
+ */
78
+ export const CLIENT_REPOSITORY = pkg.repository?.url.replace(/\.git$/, "") ?? "https://github.com/ryanwtf7/ryanlink";
79
+
80
+ /**
81
+ * Full package information
82
+ */
83
+ export const PACKAGE_INFO = Object.freeze({
84
+ name: CLIENT_NAME,
85
+ version: CLIENT_VERSION,
86
+ repository: CLIENT_REPOSITORY,
87
+ description: pkg.description,
88
+ });