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,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
+ }