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,176 @@
1
+ import type { NonNullableProp } from "../common";
2
+ import type { CreateQueueOptions } from "../audio";
3
+
4
+ /**
5
+ * Common info in Discord's 'dispatch' payload type
6
+ */
7
+ export interface CommonDispatchPayloadInfo {
8
+ op: 0;
9
+ s: number;
10
+ }
11
+
12
+ /**
13
+ * Discord client ready payload (partial, essential only)
14
+ */
15
+ export interface BotReadyPayload extends CommonDispatchPayloadInfo {
16
+ t: "READY";
17
+ d: {
18
+ user: {
19
+ id: string;
20
+ };
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Discord voice state update payload
26
+ */
27
+ export interface VoiceStateUpdatePayload extends CommonDispatchPayloadInfo {
28
+ t: "VOICE_STATE_UPDATE";
29
+ d: {
30
+ guild_id?: string;
31
+ channel_id: string | null;
32
+ user_id: string;
33
+ session_id: string;
34
+ deaf: boolean;
35
+ mute: boolean;
36
+ self_deaf: boolean;
37
+ self_mute: boolean;
38
+ suppress: boolean;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Discord voice server update payload
44
+ */
45
+ export interface VoiceServerUpdatePayload extends CommonDispatchPayloadInfo {
46
+ t: "VOICE_SERVER_UPDATE";
47
+ d: {
48
+ token: string;
49
+ guild_id: string;
50
+ endpoint: string | null;
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Discord dispatch payload
56
+ */
57
+ export type DiscordDispatchPayload = BotReadyPayload | VoiceStateUpdatePayload | VoiceServerUpdatePayload;
58
+
59
+ /**
60
+ * Internal ref representing client-side voice state
61
+ */
62
+ export interface BotVoiceState
63
+ extends
64
+ Required<NonNullableProp<Omit<VoiceStateUpdatePayload["d"], "guild_id" | "user_id">, "channel_id">>,
65
+ NonNullableProp<Omit<VoiceServerUpdatePayload["d"], "guild_id">, "endpoint"> {
66
+ /**
67
+ * Whether the voice connection is established
68
+ */
69
+ connected: boolean;
70
+
71
+ /**
72
+ * The node session ID this voice state is connected to
73
+ */
74
+ node_session_id: string;
75
+
76
+ /**
77
+ * Whether the voice connection is reconnecting
78
+ */
79
+ reconnecting: boolean;
80
+
81
+ /**
82
+ * The voice region ID (extracted from endpoint)
83
+ */
84
+ region_id: string;
85
+ }
86
+
87
+ /**
88
+ * Options for customizing the player while connecting
89
+ */
90
+ export interface ConnectOptions extends Pick<CreateQueueOptions, "context" | "filters" | "node" | "volume"> {}
91
+
92
+ /**
93
+ * Discord voice close event codes
94
+ * https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes
95
+ */
96
+ export const enum VoiceCloseCodes {
97
+ /**
98
+ * You sent an invalid opcode.
99
+ */
100
+ UnknownOpcode = 4001,
101
+
102
+ /**
103
+ * You sent an invalid payload in your identifying to the Gateway.
104
+ */
105
+ FailedToDecodePayload,
106
+
107
+ /**
108
+ * You sent a payload before identifying with the Gateway.
109
+ */
110
+ NotAuthenticated,
111
+
112
+ /**
113
+ * The token you sent in your identify payload is incorrect.
114
+ */
115
+ AuthenticationFailed,
116
+
117
+ /**
118
+ * You sent more than one identify payload.
119
+ */
120
+ AlreadyAuthenticated,
121
+
122
+ /**
123
+ * Your session is no longer valid.
124
+ */
125
+ SessionNoLongerValid,
126
+
127
+ /**
128
+ * Your session has timed out.
129
+ */
130
+ SessionTimeout = 4009,
131
+
132
+ /**
133
+ * We can't find the server you're trying to connect to.
134
+ */
135
+ ServerNotFound = 4011,
136
+
137
+ /**
138
+ * We didn't recognize the protocol you sent.
139
+ */
140
+ UnknownProtocol,
141
+
142
+ /**
143
+ * Disconnect individual client (you were kicked, the main gateway session was dropped, etc.). Should not reconnect.
144
+ */
145
+ Disconnected = 4014,
146
+
147
+ /**
148
+ * The server crashed. Our bad! Try resuming.
149
+ */
150
+ VoiceServerCrashed,
151
+
152
+ /**
153
+ * We didn't recognize your encryption.
154
+ */
155
+ UnknownEncryptionMode,
156
+
157
+ /**
158
+ * This channel requires a client supporting E2EE via the DAVE Protocol
159
+ */
160
+ DAVEProtocolRequired,
161
+
162
+ /**
163
+ * You sent a malformed request.
164
+ */
165
+ BadRequest = 4020,
166
+
167
+ /**
168
+ * Disconnect due to rate limit exceeded. Should not reconnect.
169
+ */
170
+ DisconnectedRateLimited,
171
+
172
+ /**
173
+ * Disconnect all clients due to call terminated (channel deleted, voice server changed, etc.). Should not reconnect.
174
+ */
175
+ DisconnectedCallTerminated,
176
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Voice type definitions
3
+ */
4
+
5
+ export * from "./VoiceManager";
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Utility functions for Ryanlink
3
+ */
4
+
5
+ /**
6
+ * Do nothing, return nothing
7
+ * Useful for error handling and default callbacks
8
+ */
9
+ export const noop = (): void => {};
10
+
11
+ /**
12
+ * Format duration as a string
13
+ * @param ms Duration in milliseconds
14
+ * @returns Formatted string: `hh:mm:ss`, `mm:ss`, or `00:00` (default)
15
+ *
16
+ * @example
17
+ * formatDuration(0) // "00:00"
18
+ * formatDuration(90000) // "01:30"
19
+ * formatDuration(3661000) // "01:01:01"
20
+ */
21
+ export const formatDuration = (ms: number): string => {
22
+ if (!Number.isSafeInteger(ms) || ms <= 0) {
23
+ return "00:00";
24
+ }
25
+
26
+ const s = Math.floor(ms / 1000);
27
+ const ss = `${s % 60}`.padStart(2, "0");
28
+ const mm = `${Math.floor(s / 60) % 60}`.padStart(2, "0");
29
+
30
+ if (s < 3600) {
31
+ return `${(s === 3600 ? "01:" : "") + mm}:${ss}`;
32
+ }
33
+
34
+ return `${`${Math.floor(s / 3600)}`.padStart(2, "0")}:${mm}:${ss}`;
35
+ };
36
+
37
+ /**
38
+ * Parse duration string to milliseconds
39
+ * @param duration Duration string (hh:mm:ss, mm:ss, or ss)
40
+ * @returns Duration in milliseconds
41
+ *
42
+ * @example
43
+ * parseDuration("01:30") // 90000
44
+ * parseDuration("1:01:01") // 3661000
45
+ */
46
+ export const parseDuration = (duration: string): number => {
47
+ const parts = duration.split(":").map(Number);
48
+
49
+ if (parts.length === 1) {
50
+ return (parts[0] ?? 0) * 1000;
51
+ }
52
+
53
+ if (parts.length === 2) {
54
+ return ((parts[0] ?? 0) * 60 + (parts[1] ?? 0)) * 1000;
55
+ }
56
+
57
+ if (parts.length === 3) {
58
+ return ((parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0)) * 1000;
59
+ }
60
+
61
+ return 0;
62
+ };
63
+
64
+ /**
65
+ * Clamp a number between min and max
66
+ * @param value Value to clamp
67
+ * @param min Minimum value
68
+ * @param max Maximum value
69
+ * @returns Clamped value
70
+ */
71
+ export const clamp = (value: number, min: number, max: number): number => {
72
+ return Math.min(Math.max(value, min), max);
73
+ };
74
+
75
+ /**
76
+ * Sleep for a specified duration
77
+ * @param ms Duration in milliseconds
78
+ * @returns Promise that resolves after the duration
79
+ */
80
+ export const sleep = (ms: number): Promise<void> => {
81
+ return new Promise((resolve) => setTimeout(resolve, ms));
82
+ };
83
+
84
+ /**
85
+ * Retry a function with exponential backoff
86
+ * @param fn Function to retry
87
+ * @param maxAttempts Maximum number of attempts
88
+ * @param baseDelay Base delay in milliseconds
89
+ * @returns Result of the function
90
+ */
91
+ export const retry = async <T>(fn: () => Promise<T>, maxAttempts = 3, baseDelay = 1000): Promise<T> => {
92
+ let lastError: Error | undefined;
93
+
94
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
95
+ try {
96
+ return await fn();
97
+ } catch (error) {
98
+ lastError = error instanceof Error ? error : new Error(String(error));
99
+
100
+ if (attempt < maxAttempts - 1) {
101
+ const delay = baseDelay * Math.pow(2, attempt);
102
+ await sleep(delay);
103
+ }
104
+ }
105
+ }
106
+
107
+ throw lastError ?? new Error("Retry failed");
108
+ };
109
+
110
+ /**
111
+ * Shuffle an array in place using Fisher-Yates algorithm
112
+ * @param array Array to shuffle
113
+ * @returns The shuffled array
114
+ */
115
+ export const shuffle = <T>(array: T[]): T[] => {
116
+ for (let i = array.length - 1; i > 0; i--) {
117
+ const j = Math.floor(Math.random() * (i + 1));
118
+ [array[i], array[j]] = [array[j], array[i]];
119
+ }
120
+ return array;
121
+ };
122
+
123
+ /**
124
+ * Get a random element from an array
125
+ * @param array Array to pick from
126
+ * @returns Random element or undefined if array is empty
127
+ */
128
+ export const randomElement = <T>(array: T[]): T | undefined => {
129
+ if (array.length === 0) {
130
+ return undefined;
131
+ }
132
+ return array[Math.floor(Math.random() * array.length)];
133
+ };
134
+
135
+ /**
136
+ * Chunk an array into smaller arrays
137
+ * @param array Array to chunk
138
+ * @param size Size of each chunk
139
+ * @returns Array of chunks
140
+ */
141
+ export const chunk = <T>(array: T[], size: number): T[][] => {
142
+ const chunks: T[][] = [];
143
+ for (let i = 0; i < array.length; i += size) {
144
+ chunks.push(array.slice(i, i + size));
145
+ }
146
+ return chunks;
147
+ };
148
+
149
+ /**
150
+ * Remove duplicates from an array
151
+ * @param array Array to deduplicate
152
+ * @param key Optional key function for complex objects
153
+ * @returns Array without duplicates
154
+ */
155
+ export const unique = <T>(array: T[], key?: (item: T) => unknown): T[] => {
156
+ if (!key) {
157
+ return [...new Set(array)];
158
+ }
159
+
160
+ const seen = new Set();
161
+ return array.filter((item) => {
162
+ const k = key(item);
163
+ if (seen.has(k)) {
164
+ return false;
165
+ }
166
+ seen.add(k);
167
+ return true;
168
+ });
169
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utility exports
3
+ */
4
+
5
+ export * from "./helpers";
6
+ export * from "./validators";
@@ -0,0 +1,184 @@
1
+ import { URL } from "node:url";
2
+ import type { CreateNodeOptions, PlayerOptions } from "../types";
3
+
4
+ /**
5
+ * Validation functions for Ryanlink
6
+ */
7
+
8
+ /**
9
+ * Check if input is a finite number
10
+ * @param input Value to check
11
+ * @param check Optional check type: `integer`, `natural` (> 0), `whole` (>= 0)
12
+ * @returns `true` if the input passed, `false` otherwise
13
+ */
14
+ export const isNumber = <T extends number>(
15
+ input: unknown,
16
+ check?: "integer" | "natural" | "whole" | "safe-int" | "positive",
17
+ ): input is T => {
18
+ if (check === undefined) {
19
+ return Number.isFinite(input);
20
+ }
21
+ if (check === "integer" || check === "safe-int") {
22
+ return Number.isSafeInteger(input);
23
+ }
24
+ if (check === "natural" || check === "positive") {
25
+ return Number.isSafeInteger(input) && (input as number) > 0;
26
+ }
27
+ if (check === "whole") {
28
+ return Number.isSafeInteger(input) && (input as number) >= 0;
29
+ }
30
+ return false;
31
+ };
32
+
33
+ /**
34
+ * Check if input is a string
35
+ * @param input Value to check
36
+ * @param check Optional check: {@linkcode RegExp}, `url` ({@linkcode URL.canParse}), `non-empty` (at least one non-whitespace character)
37
+ * @returns `true` if the input passed, `false` otherwise
38
+ */
39
+ export const isString = <T extends string>(input: unknown, check?: "url" | "non-empty" | RegExp): input is T => {
40
+ if (typeof input !== "string") {
41
+ return false;
42
+ }
43
+ if (check === undefined) {
44
+ return true;
45
+ }
46
+ if (check === "url") {
47
+ return URL.canParse(input);
48
+ }
49
+ if (check === "non-empty") {
50
+ return input.trim().length > 0;
51
+ }
52
+ if (check instanceof RegExp) {
53
+ return check.test(input);
54
+ }
55
+ return false;
56
+ };
57
+
58
+ /**
59
+ * Check if input is a plain object
60
+ * @param input Value to check
61
+ * @param check Optional check: `non-empty` (at least one key)
62
+ * @returns `true` if the input passed, `false` otherwise
63
+ */
64
+ export const isRecord = <T extends Record<string, unknown>>(input: unknown, check?: "non-empty"): input is T => {
65
+ if (!input || input.constructor !== Object) {
66
+ return false;
67
+ }
68
+ if (check === undefined) {
69
+ return true;
70
+ }
71
+ if (check === "non-empty") {
72
+ return Object.keys(input).length > 0;
73
+ }
74
+ return false;
75
+ };
76
+
77
+ /**
78
+ * Check if input is an array
79
+ * @param input Value to check
80
+ * @param check Optional check: `non-empty`, or a function (same as {@linkcode Array.prototype.every})
81
+ * @returns `true` if the input passed, `false` otherwise
82
+ */
83
+ export const isArray = <T extends unknown[]>(
84
+ input: unknown,
85
+ check?: "non-empty" | Parameters<T["every"]>[0],
86
+ ): input is T => {
87
+ if (!Array.isArray(input)) {
88
+ return false;
89
+ }
90
+ if (check === undefined) {
91
+ return true;
92
+ }
93
+ if (check === "non-empty") {
94
+ return input.length !== 0;
95
+ }
96
+ if (typeof check === "function") {
97
+ return input.every(check);
98
+ }
99
+ return false;
100
+ };
101
+
102
+ /**
103
+ * Check if input is a boolean
104
+ * @param input Value to check
105
+ * @returns `true` if the input is a boolean, `false` otherwise
106
+ */
107
+ export const isBoolean = (input: unknown): input is boolean => {
108
+ return typeof input === "boolean";
109
+ };
110
+
111
+ /**
112
+ * Check if input is a function
113
+ * @param input Value to check
114
+ * @returns `true` if the input is a function, `false` otherwise
115
+ */
116
+ export const isFunction = (input: unknown): input is (...args: unknown[]) => unknown => {
117
+ return typeof input === "function";
118
+ };
119
+
120
+ /**
121
+ * Check if input is null or undefined
122
+ * @param input Value to check
123
+ * @returns `true` if the input is null or undefined, `false` otherwise
124
+ */
125
+ export const isNullish = (input: unknown): input is null | undefined => {
126
+ return input === null || input === undefined;
127
+ };
128
+
129
+ /**
130
+ * Check if input is a valid snowflake (Discord ID)
131
+ * @param input Value to check
132
+ * @returns `true` if the input is a valid snowflake, `false` otherwise
133
+ */
134
+ export const isSnowflake = (input: unknown): input is string => {
135
+ return isString(input) && /^\d{17,20}$/.test(input);
136
+ };
137
+
138
+ /**
139
+ * Check if input is a valid URL
140
+ * @param input Value to check
141
+ * @returns `true` if the input is a valid URL, `false` otherwise
142
+ */
143
+ export const isUrl = (input: unknown): input is string => {
144
+ return isString(input, "url");
145
+ };
146
+
147
+ /**
148
+ * Type guard to check if error is an Error instance
149
+ * @param error Value to check
150
+ * @returns `true` if the value is an Error, `false` otherwise
151
+ */
152
+ export const isError = (error: unknown): error is Error => {
153
+ return error instanceof Error;
154
+ };
155
+
156
+ /**
157
+ * Assert that a condition is true, throw error otherwise
158
+ */
159
+ export function assert(condition: unknown, message: string): asserts condition {
160
+ if (!condition) {
161
+ throw new Error(message);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Validates node options
167
+ */
168
+ export function validateNodeOptions(options: CreateNodeOptions): void {
169
+ assert(isString(options.name, "non-empty"), "Node name must be a non-empty string");
170
+ assert(isString(options.host, "non-empty"), "Node host must be a non-empty string");
171
+ assert(isNumber(options.port, "natural"), "Node port must be a positive number");
172
+ assert(isString(options.password, "non-empty"), "Node password must be a non-empty string");
173
+ }
174
+
175
+ /**
176
+ * Validates player options
177
+ */
178
+ export function validatePlayerOptions(options: PlayerOptions): void {
179
+ assert(isArray(options.nodes, "non-empty"), "At least one node is required");
180
+ assert(isFunction(options.forwardVoiceUpdate), "forwardVoiceUpdate must be a function");
181
+ if (options.queryPrefix !== undefined) {
182
+ assert(isString(options.queryPrefix, "non-empty"), "queryPrefix must be a non-empty string");
183
+ }
184
+ }