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,326 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { LavalinkNode } from "./LavalinkConnection";
|
|
3
|
+
import type { Player } from "../core/RyanlinkPlayer";
|
|
4
|
+
import type { NodeOptions, NodeManagerEvents, LavalinkInfo } from "../types/lavalink";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Node metrics for selection and monitoring
|
|
8
|
+
*/
|
|
9
|
+
export interface NodeMetrics {
|
|
10
|
+
/** Total number of nodes */
|
|
11
|
+
total: number;
|
|
12
|
+
/** Number of connected nodes */
|
|
13
|
+
connected: number;
|
|
14
|
+
/** Number of ready nodes */
|
|
15
|
+
ready: number;
|
|
16
|
+
/** Number of reconnecting nodes */
|
|
17
|
+
reconnecting: number;
|
|
18
|
+
/** Total players across all nodes */
|
|
19
|
+
players: number;
|
|
20
|
+
/** Total playing players across all nodes */
|
|
21
|
+
playingPlayers: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Manages all Lavalink nodes
|
|
26
|
+
* Handles node creation, connection, and selection
|
|
27
|
+
*/
|
|
28
|
+
export class NodeManager extends EventEmitter<NodeManagerEvents> {
|
|
29
|
+
#player: Player;
|
|
30
|
+
#nodes = new Map<string, LavalinkNode>();
|
|
31
|
+
|
|
32
|
+
readonly info = new Map<string, LavalinkInfo>(); // LavalinkInfo cache
|
|
33
|
+
|
|
34
|
+
constructor(player: Player) {
|
|
35
|
+
super({ captureRejections: false });
|
|
36
|
+
this.#player = player;
|
|
37
|
+
|
|
38
|
+
const immutable: PropertyDescriptor = {
|
|
39
|
+
writable: false,
|
|
40
|
+
configurable: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
Object.defineProperties(this, {
|
|
44
|
+
info: immutable,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get a node by name
|
|
50
|
+
*/
|
|
51
|
+
get(name: string): LavalinkNode | undefined {
|
|
52
|
+
return this.#nodes.get(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a node exists
|
|
57
|
+
*/
|
|
58
|
+
has(name: string): boolean {
|
|
59
|
+
return this.#nodes.has(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all nodes
|
|
64
|
+
*/
|
|
65
|
+
get all(): LavalinkNode[] {
|
|
66
|
+
return Array.from(this.#nodes.values());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get number of nodes
|
|
71
|
+
*/
|
|
72
|
+
get size(): number {
|
|
73
|
+
return this.#nodes.size;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a new node
|
|
78
|
+
*/
|
|
79
|
+
create(options: NodeOptions): LavalinkNode {
|
|
80
|
+
if (this.#nodes.has(options.name)) {
|
|
81
|
+
throw new Error(`Node '${options.name}' already exists`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const node = new LavalinkNode({
|
|
85
|
+
...options,
|
|
86
|
+
clientId: this.#player.clientId ?? "",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Forward node events
|
|
90
|
+
node.on("connect", (_attempts, _name) => {
|
|
91
|
+
this.emit("connect", node);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
node.on("ready", (resumed, sessionId, _name) => {
|
|
95
|
+
this.emit("ready", node, resumed, sessionId);
|
|
96
|
+
|
|
97
|
+
// Fetch node info on first ready (not on resume)
|
|
98
|
+
if (!this.info.has(node.name)) {
|
|
99
|
+
this.fetchInfo(node.name).catch(() => {});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (resumed) {
|
|
103
|
+
void this.#handleResumed(node);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
node.on("disconnect", (code, reason, _byLocal, _name) => {
|
|
108
|
+
this.emit("disconnect", node, { code, reason });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
node.on("close", (_code, _reason, _name) => {
|
|
112
|
+
// Node is reconnecting
|
|
113
|
+
this.emit("reconnecting", node);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
node.on("error", (error, _name) => {
|
|
117
|
+
this.emit("error", node, error);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
node.on("dispatch", (payload, _name) => {
|
|
121
|
+
this.emit("raw", node, payload);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
this.#nodes.set(options.name, node);
|
|
125
|
+
this.emit("create", node);
|
|
126
|
+
|
|
127
|
+
return node;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Delete a node
|
|
132
|
+
*/
|
|
133
|
+
async delete(name: string): Promise<boolean> {
|
|
134
|
+
const node = this.#nodes.get(name);
|
|
135
|
+
if (!node) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if node has active queues
|
|
140
|
+
const activeQueues = this.#player.queues.all.filter((q) => q.node?.name === name);
|
|
141
|
+
if (activeQueues.length > 0) {
|
|
142
|
+
throw new Error(`Cannot delete node '${name}' with ${activeQueues.length} active queue(s)`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await node.disconnect();
|
|
146
|
+
this.#nodes.delete(name);
|
|
147
|
+
this.info.delete(name);
|
|
148
|
+
this.emit("destroy", node);
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Connect all nodes
|
|
155
|
+
*/
|
|
156
|
+
async connect(): Promise<void> {
|
|
157
|
+
const promises = Array.from(this.#nodes.values()).map((node) => node.connect());
|
|
158
|
+
await Promise.allSettled(promises);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Disconnect all nodes
|
|
163
|
+
*/
|
|
164
|
+
async disconnect(): Promise<void> {
|
|
165
|
+
const promises = Array.from(this.#nodes.values()).map((node) => node.disconnect());
|
|
166
|
+
await Promise.allSettled(promises);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get relevant nodes sorted by load and availability
|
|
171
|
+
* Returns nodes that are ready and have lowest load
|
|
172
|
+
*/
|
|
173
|
+
relevant(): LavalinkNode[] {
|
|
174
|
+
const readyNodes = Array.from(this.#nodes.values()).filter((node) => node.ready);
|
|
175
|
+
|
|
176
|
+
if (readyNodes.length === 0) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sort by load (players, CPU, memory)
|
|
181
|
+
return readyNodes.sort((a, b) => {
|
|
182
|
+
const aStats = a.stats;
|
|
183
|
+
const bStats = b.stats;
|
|
184
|
+
|
|
185
|
+
if (!aStats) {
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
if (!bStats) {
|
|
189
|
+
return -1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Compare by playing players first
|
|
193
|
+
const aLoad = aStats.playingPlayers / (aStats.players || 1);
|
|
194
|
+
const bLoad = bStats.playingPlayers / (bStats.players || 1);
|
|
195
|
+
|
|
196
|
+
if (aLoad !== bLoad) {
|
|
197
|
+
return aLoad - bLoad;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Then by CPU load
|
|
201
|
+
const aCpu = aStats.cpu.lavalinkLoad;
|
|
202
|
+
const bCpu = bStats.cpu.lavalinkLoad;
|
|
203
|
+
|
|
204
|
+
if (aCpu !== bCpu) {
|
|
205
|
+
return aCpu - bCpu;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Finally by memory usage
|
|
209
|
+
const aMemory = aStats.memory.used / aStats.memory.allocated;
|
|
210
|
+
const bMemory = bStats.memory.used / bStats.memory.allocated;
|
|
211
|
+
|
|
212
|
+
return aMemory - bMemory;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get node metrics
|
|
218
|
+
*/
|
|
219
|
+
get metrics(): NodeMetrics {
|
|
220
|
+
const nodes = Array.from(this.#nodes.values());
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
total: nodes.length,
|
|
224
|
+
connected: nodes.filter((n) => n.connected).length,
|
|
225
|
+
ready: nodes.filter((n) => n.ready).length,
|
|
226
|
+
reconnecting: nodes.filter((n) => n.reconnecting).length,
|
|
227
|
+
players: nodes.reduce((sum, n) => sum + (n.stats?.players ?? 0), 0),
|
|
228
|
+
playingPlayers: nodes.reduce((sum, n) => sum + (n.stats?.playingPlayers ?? 0), 0),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get all node names
|
|
234
|
+
*/
|
|
235
|
+
keys(): IterableIterator<string> {
|
|
236
|
+
return this.#nodes.keys();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get all nodes
|
|
241
|
+
*/
|
|
242
|
+
values(): IterableIterator<LavalinkNode> {
|
|
243
|
+
return this.#nodes.values();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get all entries
|
|
248
|
+
*/
|
|
249
|
+
entries(): IterableIterator<[string, LavalinkNode]> {
|
|
250
|
+
return this.#nodes.entries();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Fetch and cache node info
|
|
255
|
+
* @param name Node name
|
|
256
|
+
*/
|
|
257
|
+
async fetchInfo(name: string): Promise<LavalinkInfo> {
|
|
258
|
+
if (this.info.has(name)) {
|
|
259
|
+
return this.info.get(name) as LavalinkInfo;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const node = this.#nodes.get(name);
|
|
263
|
+
if (!node) {
|
|
264
|
+
throw new Error(`Node '${name}' not found`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const info = await node.rest.fetchInfo();
|
|
268
|
+
this.info.set(name, info);
|
|
269
|
+
return info;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check if a feature is supported by a node
|
|
274
|
+
* @param type Feature type (filter, source, plugin)
|
|
275
|
+
* @param value Feature value (name)
|
|
276
|
+
* @param nodeName Optional node name (checks all if not specified)
|
|
277
|
+
*/
|
|
278
|
+
supports(type: "filter" | "source" | "plugin", value: string, nodeName?: string): boolean {
|
|
279
|
+
if (nodeName) {
|
|
280
|
+
const node = this.#nodes.get(nodeName);
|
|
281
|
+
if (!node) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
return this.#checkNodeSupport(node.name, type, value);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if any node supports it
|
|
288
|
+
return Array.from(this.#nodes.values()).some((node) => this.#checkNodeSupport(node.name, type, value));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Check if a specific node supports a feature
|
|
293
|
+
*/
|
|
294
|
+
#checkNodeSupport(nodeName: string, type: "filter" | "source" | "plugin", value: string): boolean {
|
|
295
|
+
const info = this.info.get(nodeName);
|
|
296
|
+
if (!info) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
switch (type) {
|
|
301
|
+
case "filter":
|
|
302
|
+
return info.filters?.includes(value) ?? false;
|
|
303
|
+
case "source":
|
|
304
|
+
return info.sourceManagers?.includes(value) ?? false;
|
|
305
|
+
case "plugin":
|
|
306
|
+
return info.plugins?.some((p: { name: string }) => p.name === value) ?? false;
|
|
307
|
+
default:
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handle node resumption
|
|
314
|
+
* Sync all queues when a node resumes
|
|
315
|
+
*/
|
|
316
|
+
async #handleResumed(node: LavalinkNode): Promise<void> {
|
|
317
|
+
try {
|
|
318
|
+
// Sync queues if autoSync is enabled
|
|
319
|
+
if (this.#player.options.autoSync) {
|
|
320
|
+
await this.#player.queues.syncAll();
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.emit("error", node, error as Error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { Routes } from "../config/endpoints";
|
|
2
|
+
import { CLIENT_NAME, CLIENT_VERSION, CLIENT_REPOSITORY } from "../metadata";
|
|
3
|
+
import type {
|
|
4
|
+
RESTOptions,
|
|
5
|
+
LoadResult,
|
|
6
|
+
APIPlayer,
|
|
7
|
+
PlayerUpdateRequestBody,
|
|
8
|
+
PlayerUpdateQueryParams,
|
|
9
|
+
SessionUpdateRequestBody,
|
|
10
|
+
SessionUpdateResponseBody,
|
|
11
|
+
LavalinkInfo,
|
|
12
|
+
NodeStats,
|
|
13
|
+
RoutePlannerStatus,
|
|
14
|
+
APITrack,
|
|
15
|
+
} from "../types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* REST client for Lavalink HTTP API
|
|
19
|
+
* Uses native fetch for better performance
|
|
20
|
+
*/
|
|
21
|
+
export class REST {
|
|
22
|
+
#origin: string;
|
|
23
|
+
#headers: Record<string, string>;
|
|
24
|
+
#sessionId: string | null = null;
|
|
25
|
+
#requestTimeout: number;
|
|
26
|
+
|
|
27
|
+
readonly userAgent: string;
|
|
28
|
+
|
|
29
|
+
constructor(options: RESTOptions) {
|
|
30
|
+
if (options.origin) {
|
|
31
|
+
if (!options.origin.startsWith("http://") && !options.origin.startsWith("https://")) {
|
|
32
|
+
throw new Error("Origin must start with http:// or https://");
|
|
33
|
+
}
|
|
34
|
+
this.#origin = options.origin;
|
|
35
|
+
} else {
|
|
36
|
+
const protocol = options.secure ? "https" : "http";
|
|
37
|
+
const port = options.port ?? 2333;
|
|
38
|
+
const host = options.host ?? "localhost";
|
|
39
|
+
this.#origin = `${protocol}://${host}:${port}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.version !== undefined && options.version <= 0) {
|
|
43
|
+
throw new Error("Version must be a positive number");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.password.includes("\n") || options.password.includes("\r")) {
|
|
47
|
+
throw new Error("Password cannot contain newline characters");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const userAgent = options.userAgent ?? `${CLIENT_NAME}/${CLIENT_VERSION} (${CLIENT_REPOSITORY})`;
|
|
51
|
+
if (userAgent.includes("\n") || userAgent.includes("\r")) {
|
|
52
|
+
throw new Error("User agent cannot contain newline characters");
|
|
53
|
+
}
|
|
54
|
+
this.userAgent = userAgent;
|
|
55
|
+
|
|
56
|
+
const requestTimeout = options.requestTimeout ?? 10_000;
|
|
57
|
+
if (requestTimeout <= 0) {
|
|
58
|
+
throw new Error("Request timeout must be a positive number");
|
|
59
|
+
}
|
|
60
|
+
this.#requestTimeout = requestTimeout;
|
|
61
|
+
|
|
62
|
+
this.#headers = {
|
|
63
|
+
Authorization: options.password,
|
|
64
|
+
"User-Agent": this.userAgent,
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (options.sessionId) {
|
|
69
|
+
this.#sessionId = options.sessionId;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get origin(): string {
|
|
74
|
+
return this.#origin;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get sessionId(): string | null {
|
|
78
|
+
return this.#sessionId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
set sessionId(value: string | null) {
|
|
82
|
+
if (value !== null && (typeof value !== "string" || value.trim() === "")) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.#sessionId = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Make a request to the Lavalink REST API
|
|
90
|
+
*/
|
|
91
|
+
async #request<T>(path: string, options: RequestInit = {}, timeout?: number): Promise<T> {
|
|
92
|
+
const url = `${this.#origin}${path}`;
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout ?? this.#requestTimeout);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
...options,
|
|
99
|
+
headers: {
|
|
100
|
+
...this.#headers,
|
|
101
|
+
...options.headers,
|
|
102
|
+
},
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const error = await this.#parseError(response);
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle 204 No Content
|
|
114
|
+
if (response.status === 204) {
|
|
115
|
+
return undefined as T;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (await response.json()) as T;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
clearTimeout(timeoutId);
|
|
121
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
122
|
+
throw new Error(`Request timeout after ${timeout ?? this.#requestTimeout}ms`);
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async #parseError(response: Response): Promise<Error> {
|
|
129
|
+
let message = `HTTP ${response.status}: ${response.statusText}`;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const data = (await response.json()) as { error?: string; message?: string };
|
|
133
|
+
if (data.error) {
|
|
134
|
+
message = data.error;
|
|
135
|
+
} else if (data.message) {
|
|
136
|
+
message = data.message;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Use default message
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const error = new Error(message);
|
|
143
|
+
error.name = "LavalinkRestError";
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Load tracks from a query or URL
|
|
149
|
+
*/
|
|
150
|
+
async loadTracks(identifier: string): Promise<LoadResult> {
|
|
151
|
+
const path = `${Routes.trackLoading()}?identifier=${encodeURIComponent(identifier)}`;
|
|
152
|
+
return this.#request<LoadResult>(path, { method: "GET" });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Decode a single track
|
|
157
|
+
*/
|
|
158
|
+
async decodeTrack(encoded: string): Promise<APITrack> {
|
|
159
|
+
const path = `${Routes.trackDecoding()}?encodedTrack=${encodeURIComponent(encoded)}`;
|
|
160
|
+
return this.#request<APITrack>(path, { method: "GET" });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Decode multiple tracks
|
|
165
|
+
*/
|
|
166
|
+
async decodeTracks(encoded: string[]): Promise<APITrack[]> {
|
|
167
|
+
return this.#request<APITrack[]>(Routes.trackDecoding(true), {
|
|
168
|
+
method: "POST",
|
|
169
|
+
body: JSON.stringify(encoded),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Fetch all players for a session
|
|
175
|
+
*/
|
|
176
|
+
async fetchPlayers(): Promise<APIPlayer[]> {
|
|
177
|
+
if (!this.#sessionId) {
|
|
178
|
+
throw new Error("No session ID available");
|
|
179
|
+
}
|
|
180
|
+
return this.#request<APIPlayer[]>(Routes.player(this.#sessionId), { method: "GET" });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Fetch a specific player
|
|
185
|
+
*/
|
|
186
|
+
async fetchPlayer(guildId: string): Promise<APIPlayer> {
|
|
187
|
+
if (!this.#sessionId) {
|
|
188
|
+
throw new Error("No session ID available");
|
|
189
|
+
}
|
|
190
|
+
return this.#request<APIPlayer>(Routes.player(this.#sessionId, guildId), { method: "GET" });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Update a player
|
|
195
|
+
*/
|
|
196
|
+
async updatePlayer(
|
|
197
|
+
guildId: string,
|
|
198
|
+
data: PlayerUpdateRequestBody,
|
|
199
|
+
params?: PlayerUpdateQueryParams,
|
|
200
|
+
): Promise<APIPlayer> {
|
|
201
|
+
if (!this.#sessionId) {
|
|
202
|
+
throw new Error("No session ID available");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let path = Routes.player(this.#sessionId, guildId);
|
|
206
|
+
if (params?.noReplace) {
|
|
207
|
+
path += "?noReplace=true";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.#request<APIPlayer>(path, {
|
|
211
|
+
method: "PATCH",
|
|
212
|
+
body: JSON.stringify(data),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Destroy a player
|
|
218
|
+
*/
|
|
219
|
+
async destroyPlayer(guildId: string): Promise<void> {
|
|
220
|
+
if (!this.#sessionId) {
|
|
221
|
+
throw new Error("No session ID available");
|
|
222
|
+
}
|
|
223
|
+
return this.#request<void>(Routes.player(this.#sessionId, guildId), { method: "DELETE" });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Update session configuration
|
|
228
|
+
*/
|
|
229
|
+
async updateSession(data: SessionUpdateRequestBody): Promise<SessionUpdateResponseBody> {
|
|
230
|
+
if (!this.#sessionId) {
|
|
231
|
+
throw new Error("No session ID available");
|
|
232
|
+
}
|
|
233
|
+
return this.#request<SessionUpdateResponseBody>(Routes.session(this.#sessionId), {
|
|
234
|
+
method: "PATCH",
|
|
235
|
+
body: JSON.stringify(data),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Fetch Lavalink server info
|
|
241
|
+
*/
|
|
242
|
+
async fetchInfo(): Promise<LavalinkInfo> {
|
|
243
|
+
return this.#request<LavalinkInfo>(Routes.info(), { method: "GET" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Fetch node statistics
|
|
248
|
+
*/
|
|
249
|
+
async fetchStats(): Promise<NodeStats> {
|
|
250
|
+
return this.#request<NodeStats>(Routes.stats(), { method: "GET" });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Fetch Lavalink version
|
|
255
|
+
*/
|
|
256
|
+
async fetchVersion(): Promise<string> {
|
|
257
|
+
return this.#request<string>("/version", { method: "GET" });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Fetch route planner status
|
|
262
|
+
*/
|
|
263
|
+
async fetchRoutePlannerStatus(): Promise<RoutePlannerStatus> {
|
|
264
|
+
return this.#request<RoutePlannerStatus>(Routes.routePlanner(), { method: "GET" });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Free a specific address from the route planner
|
|
269
|
+
*/
|
|
270
|
+
async freeRoutePlannerAddress(address: string): Promise<void> {
|
|
271
|
+
return this.#request<void>(Routes.routePlanner("address"), {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: JSON.stringify({ address }),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Free all addresses from the route planner
|
|
279
|
+
*/
|
|
280
|
+
async freeAllRoutePlannerAddresses(): Promise<void> {
|
|
281
|
+
return this.#request<void>(Routes.routePlanner("all"), { method: "POST" });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Set SponsorBlock segments for a player
|
|
286
|
+
*/
|
|
287
|
+
async setSponsorBlock(guildId: string, segments: string[]): Promise<void> {
|
|
288
|
+
if (!this.#sessionId) {
|
|
289
|
+
throw new Error("No session ID available");
|
|
290
|
+
}
|
|
291
|
+
return this.#request<void>(`${Routes.player(this.#sessionId, guildId)}/sponsorblock`, {
|
|
292
|
+
method: "PATCH",
|
|
293
|
+
body: JSON.stringify(segments),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get current SponsorBlock segments for a player
|
|
299
|
+
*/
|
|
300
|
+
async getSponsorBlock(guildId: string): Promise<string[]> {
|
|
301
|
+
if (!this.#sessionId) {
|
|
302
|
+
throw new Error("No session ID available");
|
|
303
|
+
}
|
|
304
|
+
return this.#request<string[]>(`${Routes.player(this.#sessionId, guildId)}/sponsorblock`, { method: "GET" });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Delete SponsorBlock configuration for a player
|
|
309
|
+
*/
|
|
310
|
+
async deleteSponsorBlock(guildId: string): Promise<void> {
|
|
311
|
+
if (!this.#sessionId) {
|
|
312
|
+
throw new Error("No session ID available");
|
|
313
|
+
}
|
|
314
|
+
return this.#request<void>(`${Routes.player(this.#sessionId, guildId)}/sponsorblock`, { method: "DELETE" });
|
|
315
|
+
}
|
|
316
|
+
}
|