magmastream 2.9.0-dev.4 → 2.9.0-dev.41
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/README.md +2 -2
- package/dist/index.d.ts +2275 -959
- package/dist/index.js +14 -1
- package/dist/statestorage/JsonQueue.js +436 -0
- package/dist/{structures/Queue.js → statestorage/MemoryQueue.js} +205 -78
- package/dist/statestorage/RedisQueue.js +427 -0
- package/dist/structures/Enums.js +259 -0
- package/dist/structures/Filters.js +54 -82
- package/dist/structures/Manager.js +813 -376
- package/dist/structures/Node.js +348 -203
- package/dist/structures/Player.js +302 -135
- package/dist/structures/Plugin.js +4 -1
- package/dist/structures/Rest.js +11 -7
- package/dist/structures/Types.js +3 -0
- package/dist/structures/Utils.js +312 -263
- package/dist/utils/managerCheck.js +19 -19
- package/dist/utils/nodeCheck.js +5 -5
- package/dist/wrappers/detritus.js +36 -0
- package/dist/wrappers/discord.js.js +29 -0
- package/dist/wrappers/eris.js +29 -0
- package/dist/wrappers/oceanic.js +29 -0
- package/dist/wrappers/seyfert.js +43 -0
- package/package.json +20 -14
package/dist/structures/Node.js
CHANGED
|
@@ -1,76 +1,70 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Node =
|
|
3
|
+
exports.Node = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const Utils_1 = require("./Utils");
|
|
6
|
-
const Manager_1 = require("./Manager");
|
|
7
6
|
const Rest_1 = require("./Rest");
|
|
8
7
|
const nodeCheck_1 = tslib_1.__importDefault(require("../utils/nodeCheck"));
|
|
9
8
|
const ws_1 = tslib_1.__importDefault(require("ws"));
|
|
10
9
|
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
11
10
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
SponsorBlockSegment["Sponsor"] = "sponsor";
|
|
15
|
-
SponsorBlockSegment["SelfPromo"] = "selfpromo";
|
|
16
|
-
SponsorBlockSegment["Interaction"] = "interaction";
|
|
17
|
-
SponsorBlockSegment["Intro"] = "intro";
|
|
18
|
-
SponsorBlockSegment["Outro"] = "outro";
|
|
19
|
-
SponsorBlockSegment["Preview"] = "preview";
|
|
20
|
-
SponsorBlockSegment["MusicOfftopic"] = "music_offtopic";
|
|
21
|
-
SponsorBlockSegment["Filler"] = "filler";
|
|
22
|
-
})(SponsorBlockSegment || (exports.SponsorBlockSegment = SponsorBlockSegment = {}));
|
|
23
|
-
const validSponsorBlocks = Object.values(SponsorBlockSegment).map((v) => v.toLowerCase());
|
|
24
|
-
const sessionIdsFilePath = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "sessionIds.json");
|
|
25
|
-
let sessionIdsMap = new Map();
|
|
26
|
-
const configDir = path_1.default.dirname(sessionIdsFilePath);
|
|
27
|
-
if (!fs_1.default.existsSync(configDir)) {
|
|
28
|
-
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
29
|
-
}
|
|
11
|
+
const Enums_1 = require("./Enums");
|
|
12
|
+
const validSponsorBlocks = Object.values(Enums_1.SponsorBlockSegment).map((v) => v.toLowerCase());
|
|
30
13
|
class Node {
|
|
14
|
+
manager;
|
|
31
15
|
options;
|
|
32
16
|
/** The socket for the node. */
|
|
33
17
|
socket = null;
|
|
34
18
|
/** The stats for the node. */
|
|
35
19
|
stats;
|
|
36
20
|
/** The manager for the node */
|
|
37
|
-
manager;
|
|
21
|
+
// public manager: Manager;
|
|
38
22
|
/** The node's session ID. */
|
|
39
23
|
sessionId;
|
|
40
24
|
/** The REST instance. */
|
|
41
25
|
rest;
|
|
42
26
|
/** Actual Lavalink information of the node. */
|
|
43
27
|
info = null;
|
|
44
|
-
|
|
28
|
+
/** Whether the node is a NodeLink. */
|
|
29
|
+
isNodeLink = false;
|
|
45
30
|
reconnectTimeout;
|
|
46
31
|
reconnectAttempts = 1;
|
|
32
|
+
redisPrefix;
|
|
33
|
+
sessionIdsFilePath;
|
|
34
|
+
sessionIdsMap = new Map();
|
|
47
35
|
/**
|
|
48
36
|
* Creates an instance of Node.
|
|
49
|
-
* @param
|
|
37
|
+
* @param manager - The manager for the node.
|
|
38
|
+
* @param options - The options for the node.
|
|
50
39
|
*/
|
|
51
|
-
constructor(options) {
|
|
40
|
+
constructor(manager, options) {
|
|
41
|
+
this.manager = manager;
|
|
52
42
|
this.options = options;
|
|
53
43
|
if (!this.manager)
|
|
54
|
-
|
|
55
|
-
if (!this.manager)
|
|
56
|
-
throw new RangeError("Manager has not been initiated.");
|
|
44
|
+
throw new RangeError("Manager instance is required.");
|
|
57
45
|
if (this.manager.nodes.has(options.identifier || options.host)) {
|
|
58
46
|
return this.manager.nodes.get(options.identifier || options.host);
|
|
59
47
|
}
|
|
60
48
|
(0, nodeCheck_1.default)(options);
|
|
61
49
|
this.options = {
|
|
62
|
-
port: 2333,
|
|
63
|
-
password: "youshallnotpass",
|
|
64
|
-
useSSL: false,
|
|
65
|
-
maxRetryAttempts: 30,
|
|
66
|
-
retryDelayMs: 60000,
|
|
67
|
-
nodePriority: 0,
|
|
68
50
|
...options,
|
|
51
|
+
host: options.host ?? "localhost",
|
|
52
|
+
port: options.port ?? 2333,
|
|
53
|
+
password: options.password ?? "youshallnotpass",
|
|
54
|
+
useSSL: options.useSSL ?? false,
|
|
55
|
+
identifier: options.identifier ?? options.host,
|
|
56
|
+
maxRetryAttempts: options.maxRetryAttempts ?? 30,
|
|
57
|
+
retryDelayMs: options.retryDelayMs ?? 60000,
|
|
58
|
+
enableSessionResumeOption: options.enableSessionResumeOption ?? false,
|
|
59
|
+
sessionTimeoutSeconds: options.sessionTimeoutSeconds ?? 60,
|
|
60
|
+
apiRequestTimeoutMs: options.apiRequestTimeoutMs ?? 10000,
|
|
61
|
+
nodePriority: options.nodePriority ?? 0,
|
|
62
|
+
isNodeLink: options.isNodeLink ?? false,
|
|
63
|
+
isBackup: options.isBackup ?? false,
|
|
69
64
|
};
|
|
70
65
|
if (this.options.useSSL) {
|
|
71
66
|
this.options.port = 443;
|
|
72
67
|
}
|
|
73
|
-
this.options.identifier = options.identifier || options.host;
|
|
74
68
|
this.stats = {
|
|
75
69
|
players: 0,
|
|
76
70
|
playingPlayers: 0,
|
|
@@ -93,38 +87,47 @@ class Node {
|
|
|
93
87
|
},
|
|
94
88
|
};
|
|
95
89
|
this.manager.nodes.set(this.options.identifier, this);
|
|
96
|
-
this.manager.emit(
|
|
90
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeCreate, this);
|
|
97
91
|
this.rest = new Rest_1.Rest(this, this.manager);
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
switch (this.manager.options.stateStorage.type) {
|
|
93
|
+
case Enums_1.StateStorageType.JSON:
|
|
94
|
+
this.sessionIdsFilePath = path_1.default.join(process.cwd(), "magmastream", "dist", "sessionData", "sessionIds.json");
|
|
95
|
+
const configDir = path_1.default.dirname(this.sessionIdsFilePath);
|
|
96
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
97
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
this.createSessionIdsFile();
|
|
100
|
+
this.createReadmeFile();
|
|
101
|
+
break;
|
|
102
|
+
case Enums_1.StateStorageType.Redis:
|
|
103
|
+
this.redisPrefix = this.manager.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
104
|
+
? this.manager.options.stateStorage.redisConfig.prefix
|
|
105
|
+
: this.manager.options.stateStorage.redisConfig.prefix ?? "magmastream:";
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
102
108
|
}
|
|
103
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* Checks if the Node is currently connected.
|
|
111
|
+
* This method returns true if the Node has an active WebSocket connection, indicating it is ready to receive and process commands.
|
|
112
|
+
*/
|
|
104
113
|
get connected() {
|
|
105
114
|
if (!this.socket)
|
|
106
115
|
return false;
|
|
107
116
|
return this.socket.readyState === ws_1.default.OPEN;
|
|
108
117
|
}
|
|
109
|
-
/** Returns the address for this node. */
|
|
118
|
+
/** Returns the full address for this node, including the host and port. */
|
|
110
119
|
get address() {
|
|
111
120
|
return `${this.options.host}:${this.options.port}`;
|
|
112
121
|
}
|
|
113
|
-
/** @hidden */
|
|
114
|
-
static init(manager) {
|
|
115
|
-
this._manager = manager;
|
|
116
|
-
}
|
|
117
122
|
/**
|
|
118
123
|
* Creates the sessionIds.json file if it doesn't exist. This file is used to
|
|
119
124
|
* store the session IDs for each node. The session IDs are used to identify
|
|
120
125
|
* the node when resuming a session.
|
|
121
126
|
*/
|
|
122
127
|
createSessionIdsFile() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Create the file with an empty object as the content
|
|
127
|
-
fs_1.default.writeFileSync(sessionIdsFilePath, JSON.stringify({}), "utf-8");
|
|
128
|
+
if (!fs_1.default.existsSync(this.sessionIdsFilePath)) {
|
|
129
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Creating sessionId file at: ${this.sessionIdsFilePath}`);
|
|
130
|
+
fs_1.default.writeFileSync(this.sessionIdsFilePath, JSON.stringify({}), "utf-8");
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
/**
|
|
@@ -135,21 +138,48 @@ class Node {
|
|
|
135
138
|
* of the node identifier and cluster ID. This allows multiple clusters to
|
|
136
139
|
* be used with the same node identifier.
|
|
137
140
|
*/
|
|
138
|
-
loadSessionIds() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.sessionId = sessionIdsMap.get(compositeKey);
|
|
141
|
+
async loadSessionIds() {
|
|
142
|
+
switch (this.manager.options.stateStorage.type) {
|
|
143
|
+
case Enums_1.StateStorageType.JSON: {
|
|
144
|
+
if (fs_1.default.existsSync(this.sessionIdsFilePath)) {
|
|
145
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from file: ${this.sessionIdsFilePath}`);
|
|
146
|
+
const sessionIdsData = fs_1.default.readFileSync(this.sessionIdsFilePath, "utf-8");
|
|
147
|
+
this.sessionIdsMap = new Map(Object.entries(JSON.parse(sessionIdsData)));
|
|
148
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
149
|
+
if (this.sessionIdsMap.has(compositeKey)) {
|
|
150
|
+
this.sessionId = this.sessionIdsMap.get(compositeKey);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
152
154
|
}
|
|
155
|
+
case Enums_1.StateStorageType.Redis:
|
|
156
|
+
const key = `${this.redisPrefix}node:sessionIds`;
|
|
157
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from Redis key: ${key}`);
|
|
158
|
+
const currentRaw = await this.manager.redis.get(key);
|
|
159
|
+
if (currentRaw) {
|
|
160
|
+
try {
|
|
161
|
+
const sessionIds = JSON.parse(currentRaw);
|
|
162
|
+
if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
|
|
163
|
+
throw new Error("[NODE] loadSessionIds invalid data type from Redis.");
|
|
164
|
+
}
|
|
165
|
+
this.sessionIdsMap = new Map(Object.entries(sessionIds));
|
|
166
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
167
|
+
if (this.sessionIdsMap.has(compositeKey)) {
|
|
168
|
+
this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
|
|
169
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Restored sessionId for ${compositeKey}: ${this.sessionId}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to parse Redis sessionIds: ${err.message}`);
|
|
174
|
+
this.sessionIdsMap = new Map();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] No sessionIds found in Redis — creating new key.`);
|
|
179
|
+
await this.manager.redis.set(key, JSON.stringify({}));
|
|
180
|
+
this.sessionIdsMap = new Map();
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
153
183
|
}
|
|
154
184
|
}
|
|
155
185
|
/**
|
|
@@ -163,15 +193,43 @@ class Node {
|
|
|
163
193
|
* of the node identifier and cluster ID. This allows multiple clusters to
|
|
164
194
|
* be used with the same node identifier.
|
|
165
195
|
*/
|
|
166
|
-
updateSessionId() {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
196
|
+
async updateSessionId() {
|
|
197
|
+
switch (this.manager.options.stateStorage.type) {
|
|
198
|
+
case Enums_1.StateStorageType.JSON: {
|
|
199
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds to file: ${this.sessionIdsFilePath}`);
|
|
200
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
201
|
+
this.sessionIdsMap.set(compositeKey, this.sessionId);
|
|
202
|
+
fs_1.default.writeFileSync(this.sessionIdsFilePath, JSON.stringify(Object.fromEntries(this.sessionIdsMap)));
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case Enums_1.StateStorageType.Redis: {
|
|
206
|
+
const key = `${this.redisPrefix}node:sessionIds`;
|
|
207
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
208
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds in Redis key: ${key}`);
|
|
209
|
+
const currentRaw = await this.manager.redis.get(key);
|
|
210
|
+
let sessionIds;
|
|
211
|
+
if (currentRaw) {
|
|
212
|
+
try {
|
|
213
|
+
sessionIds = JSON.parse(currentRaw);
|
|
214
|
+
if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
|
|
215
|
+
throw new Error("Invalid data type in Redis");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Corrupted Redis sessionIds, reinitializing: ${err.message}`);
|
|
220
|
+
sessionIds = {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Redis key not found — creating new sessionIds key.`);
|
|
225
|
+
sessionIds = {};
|
|
226
|
+
}
|
|
227
|
+
sessionIds[compositeKey] = this.sessionId;
|
|
228
|
+
this.sessionIdsMap = new Map(Object.entries(sessionIds));
|
|
229
|
+
await this.manager.redis.set(key, JSON.stringify(sessionIds));
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
175
233
|
}
|
|
176
234
|
/**
|
|
177
235
|
* Connects to the Node.
|
|
@@ -182,7 +240,8 @@ class Node {
|
|
|
182
240
|
* If the node has no session ID but the `enableSessionResumeOption` option is true, it will use the session ID
|
|
183
241
|
* stored in the sessionIds.json file if it exists.
|
|
184
242
|
*/
|
|
185
|
-
connect() {
|
|
243
|
+
async connect() {
|
|
244
|
+
await this.loadSessionIds();
|
|
186
245
|
if (this.connected)
|
|
187
246
|
return;
|
|
188
247
|
const headers = {
|
|
@@ -194,13 +253,14 @@ class Node {
|
|
|
194
253
|
if (this.sessionId) {
|
|
195
254
|
headers["Session-Id"] = this.sessionId;
|
|
196
255
|
}
|
|
197
|
-
else if (this.options.enableSessionResumeOption && sessionIdsMap.has(compositeKey)) {
|
|
198
|
-
this.sessionId = sessionIdsMap.get(compositeKey) || null;
|
|
256
|
+
else if (this.options.enableSessionResumeOption && this.sessionIdsMap.has(compositeKey)) {
|
|
257
|
+
this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
|
|
199
258
|
headers["Session-Id"] = this.sessionId;
|
|
200
259
|
}
|
|
201
260
|
this.socket = new ws_1.default(`ws${this.options.useSSL ? "s" : ""}://${this.address}/v4/websocket`, { headers });
|
|
202
261
|
this.socket.on("open", this.open.bind(this));
|
|
203
262
|
this.socket.on("close", this.close.bind(this));
|
|
263
|
+
this.socket.on("upgrade", (request) => this.upgrade(request));
|
|
204
264
|
this.socket.on("message", this.message.bind(this));
|
|
205
265
|
this.socket.on("error", this.error.bind(this));
|
|
206
266
|
const debugInfo = {
|
|
@@ -214,7 +274,7 @@ class Node {
|
|
|
214
274
|
identifier: this.options.identifier,
|
|
215
275
|
},
|
|
216
276
|
};
|
|
217
|
-
this.manager.emit(
|
|
277
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connecting ${JSON.stringify(debugInfo)}`);
|
|
218
278
|
}
|
|
219
279
|
/**
|
|
220
280
|
* Destroys the node and cleans up associated resources.
|
|
@@ -229,7 +289,6 @@ class Node {
|
|
|
229
289
|
async destroy() {
|
|
230
290
|
if (!this.connected)
|
|
231
291
|
return;
|
|
232
|
-
// Emit a debug event indicating that the node is being destroyed
|
|
233
292
|
const debugInfo = {
|
|
234
293
|
connected: this.connected,
|
|
235
294
|
identifier: this.options.identifier,
|
|
@@ -237,25 +296,20 @@ class Node {
|
|
|
237
296
|
sessionId: this.sessionId,
|
|
238
297
|
playerCount: this.manager.players.filter((p) => p.node == this).size,
|
|
239
298
|
};
|
|
240
|
-
this.manager.emit(
|
|
299
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Destroying node: ${JSON.stringify(debugInfo)}`);
|
|
241
300
|
// Automove all players connected to that node
|
|
242
301
|
const players = this.manager.players.filter((p) => p.node == this);
|
|
243
302
|
if (players.size) {
|
|
244
|
-
players.
|
|
303
|
+
for (const player of players.values()) {
|
|
245
304
|
await player.autoMoveNode();
|
|
246
|
-
}
|
|
305
|
+
}
|
|
247
306
|
}
|
|
248
|
-
// Close the WebSocket connection
|
|
249
307
|
this.socket.close(1000, "destroy");
|
|
250
|
-
// Remove all event listeners on the WebSocket
|
|
251
308
|
this.socket.removeAllListeners();
|
|
252
|
-
// Clear the reconnect timeout
|
|
253
309
|
this.reconnectAttempts = 1;
|
|
254
310
|
clearTimeout(this.reconnectTimeout);
|
|
255
|
-
|
|
256
|
-
this.manager.
|
|
257
|
-
// Destroy the node from the manager
|
|
258
|
-
await this.manager.destroyNode(this.options.identifier);
|
|
311
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeDestroy, this);
|
|
312
|
+
this.manager.nodes.delete(this.options.identifier);
|
|
259
313
|
}
|
|
260
314
|
/**
|
|
261
315
|
* Attempts to reconnect to the node if the connection is lost.
|
|
@@ -274,7 +328,6 @@ class Node {
|
|
|
274
328
|
* @emits {nodeDestroy} - Emits a nodeDestroy event if the maximum number of retry attempts is reached.
|
|
275
329
|
*/
|
|
276
330
|
async reconnect() {
|
|
277
|
-
// Collect debug information regarding the current state of the node
|
|
278
331
|
const debugInfo = {
|
|
279
332
|
identifier: this.options.identifier,
|
|
280
333
|
connected: this.connected,
|
|
@@ -282,27 +335,28 @@ class Node {
|
|
|
282
335
|
maxRetryAttempts: this.options.maxRetryAttempts,
|
|
283
336
|
retryDelayMs: this.options.retryDelayMs,
|
|
284
337
|
};
|
|
285
|
-
|
|
286
|
-
this.manager.emit(Manager_1.ManagerEventTypes.Debug, `[NODE] Reconnecting node: ${JSON.stringify(debugInfo)}`);
|
|
287
|
-
// Schedule the reconnection attempt after the specified retry delay
|
|
338
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Reconnecting node: ${JSON.stringify(debugInfo)}`);
|
|
288
339
|
this.reconnectTimeout = setTimeout(async () => {
|
|
289
|
-
// Check if the maximum number of retry attempts has been reached
|
|
290
340
|
if (this.reconnectAttempts >= this.options.maxRetryAttempts) {
|
|
291
|
-
// Emit an error event and destroy the node if retries are exhausted
|
|
292
341
|
const error = new Error(`Unable to connect after ${this.options.maxRetryAttempts} attempts.`);
|
|
293
|
-
this.manager.emit(
|
|
342
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
294
343
|
return await this.destroy();
|
|
295
344
|
}
|
|
296
|
-
// Remove all listeners from the current WebSocket and reset it
|
|
297
345
|
this.socket?.removeAllListeners();
|
|
298
346
|
this.socket = null;
|
|
299
|
-
|
|
300
|
-
this.manager.emit(Manager_1.ManagerEventTypes.NodeReconnect, this);
|
|
347
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeReconnect, this);
|
|
301
348
|
this.connect();
|
|
302
|
-
// Increment the reconnect attempts counter
|
|
303
349
|
this.reconnectAttempts++;
|
|
304
350
|
}, this.options.retryDelayMs);
|
|
305
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Upgrades the node to a NodeLink.
|
|
354
|
+
*
|
|
355
|
+
* @param request - The incoming message.
|
|
356
|
+
*/
|
|
357
|
+
upgrade(request) {
|
|
358
|
+
this.isNodeLink = this.options.isNodeLink ?? Boolean(request.headers.isnodelink) ?? false;
|
|
359
|
+
}
|
|
306
360
|
/**
|
|
307
361
|
* Handles the "open" event emitted by the WebSocket connection.
|
|
308
362
|
*
|
|
@@ -312,18 +366,18 @@ class Node {
|
|
|
312
366
|
* with the node as the argument.
|
|
313
367
|
*/
|
|
314
368
|
open() {
|
|
315
|
-
// Clear any existing reconnect timeouts
|
|
316
369
|
if (this.reconnectTimeout)
|
|
317
370
|
clearTimeout(this.reconnectTimeout);
|
|
318
|
-
// Collect debug information regarding the current state of the node
|
|
319
371
|
const debugInfo = {
|
|
320
372
|
identifier: this.options.identifier,
|
|
321
373
|
connected: this.connected,
|
|
322
374
|
};
|
|
323
|
-
|
|
324
|
-
this.manager.emit(
|
|
325
|
-
|
|
326
|
-
|
|
375
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connected node: ${JSON.stringify(debugInfo)}`);
|
|
376
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeConnect, this);
|
|
377
|
+
const playersOnBackupNode = this.manager.players.filter((p) => p.node.options.isBackup);
|
|
378
|
+
if (playersOnBackupNode.size) {
|
|
379
|
+
Promise.all(Array.from(playersOnBackupNode.values(), (player) => player.moveNode(this.options.identifier)));
|
|
380
|
+
}
|
|
327
381
|
}
|
|
328
382
|
/**
|
|
329
383
|
* Handles the "close" event emitted by the WebSocket connection.
|
|
@@ -344,18 +398,21 @@ class Node {
|
|
|
344
398
|
code,
|
|
345
399
|
reason,
|
|
346
400
|
};
|
|
347
|
-
|
|
348
|
-
this.manager.emit(
|
|
349
|
-
// Emit a debug event indicating the node is disconnected
|
|
350
|
-
this.manager.emit(Manager_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${JSON.stringify(debugInfo)}`);
|
|
351
|
-
// Try moving all players connected to that node to a useable one
|
|
401
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeDisconnect, this, { code, reason });
|
|
402
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${JSON.stringify(debugInfo)}`);
|
|
352
403
|
if (this.manager.useableNode) {
|
|
353
404
|
const players = this.manager.players.filter((p) => p.node.options.identifier == this.options.identifier);
|
|
354
405
|
if (players.size) {
|
|
355
406
|
await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
|
|
356
407
|
}
|
|
357
408
|
}
|
|
358
|
-
|
|
409
|
+
else {
|
|
410
|
+
const backUpNodes = this.manager.nodes.filter((node) => node.options.isBackup && node.connected);
|
|
411
|
+
const backupNode = backUpNodes.first();
|
|
412
|
+
if (backupNode) {
|
|
413
|
+
await Promise.all(Array.from(this.manager.players.values(), (player) => player.moveNode(backupNode.options.identifier)));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
359
416
|
if (code !== 1000 || reason !== "destroy")
|
|
360
417
|
await this.reconnect();
|
|
361
418
|
}
|
|
@@ -370,15 +427,12 @@ class Node {
|
|
|
370
427
|
error(error) {
|
|
371
428
|
if (!error)
|
|
372
429
|
return;
|
|
373
|
-
// Collect debug information regarding the error
|
|
374
430
|
const debugInfo = {
|
|
375
431
|
identifier: this.options.identifier,
|
|
376
432
|
error: error.message,
|
|
377
433
|
};
|
|
378
|
-
|
|
379
|
-
this.manager.emit(
|
|
380
|
-
// Emit a "nodeError" event with the node and the error as arguments
|
|
381
|
-
this.manager.emit(Manager_1.ManagerEventTypes.NodeError, this, error);
|
|
434
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Error on node: ${JSON.stringify(debugInfo)}`);
|
|
435
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
382
436
|
}
|
|
383
437
|
/**
|
|
384
438
|
* Handles incoming messages from the Lavalink WebSocket connection.
|
|
@@ -397,7 +451,7 @@ class Node {
|
|
|
397
451
|
const payload = JSON.parse(d.toString());
|
|
398
452
|
if (!payload.op)
|
|
399
453
|
return;
|
|
400
|
-
this.manager.emit(
|
|
454
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeRaw, payload);
|
|
401
455
|
let player;
|
|
402
456
|
switch (payload.op) {
|
|
403
457
|
case "stats":
|
|
@@ -417,29 +471,27 @@ class Node {
|
|
|
417
471
|
if (player && player.node.options.identifier !== this.options.identifier) {
|
|
418
472
|
return;
|
|
419
473
|
}
|
|
420
|
-
this.manager.emit(
|
|
474
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${JSON.stringify(payload)}`);
|
|
421
475
|
await this.handleEvent(payload);
|
|
422
476
|
break;
|
|
423
477
|
case "ready":
|
|
424
|
-
this.manager.emit(
|
|
478
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${JSON.stringify(payload)}`);
|
|
425
479
|
this.rest.setSessionId(payload.sessionId);
|
|
426
480
|
this.sessionId = payload.sessionId;
|
|
427
|
-
this.updateSessionId();
|
|
481
|
+
await this.updateSessionId();
|
|
428
482
|
this.info = await this.fetchInfo();
|
|
429
|
-
// Log if the session was resumed successfully
|
|
430
483
|
if (payload.resumed) {
|
|
431
|
-
// Load player states from the JSON file
|
|
432
484
|
await this.manager.loadPlayerStates(this.options.identifier);
|
|
433
485
|
}
|
|
434
486
|
if (this.options.enableSessionResumeOption) {
|
|
435
487
|
await this.rest.patch(`/v4/sessions/${this.sessionId}`, {
|
|
436
488
|
resuming: this.options.enableSessionResumeOption,
|
|
437
|
-
timeout: this.options.
|
|
489
|
+
timeout: this.options.sessionTimeoutSeconds,
|
|
438
490
|
});
|
|
439
491
|
}
|
|
440
492
|
break;
|
|
441
493
|
default:
|
|
442
|
-
this.manager.emit(
|
|
494
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Unexpected op "${payload.op}" with data: ${payload.message}`));
|
|
443
495
|
return;
|
|
444
496
|
}
|
|
445
497
|
}
|
|
@@ -455,7 +507,7 @@ class Node {
|
|
|
455
507
|
const player = this.manager.players.get(payload.guildId);
|
|
456
508
|
if (!player)
|
|
457
509
|
return;
|
|
458
|
-
const track = player.queue.
|
|
510
|
+
const track = await player.queue.getCurrent();
|
|
459
511
|
const type = payload.type;
|
|
460
512
|
let error;
|
|
461
513
|
switch (type) {
|
|
@@ -478,20 +530,29 @@ class Node {
|
|
|
478
530
|
this.socketClosed(player, payload);
|
|
479
531
|
break;
|
|
480
532
|
case "SegmentsLoaded":
|
|
481
|
-
this.sponsorBlockSegmentLoaded(player,
|
|
533
|
+
this.sponsorBlockSegmentLoaded(player, track, payload);
|
|
482
534
|
break;
|
|
483
535
|
case "SegmentSkipped":
|
|
484
|
-
this.sponsorBlockSegmentSkipped(player,
|
|
536
|
+
this.sponsorBlockSegmentSkipped(player, track, payload);
|
|
485
537
|
break;
|
|
486
538
|
case "ChaptersLoaded":
|
|
487
|
-
this.sponsorBlockChaptersLoaded(player,
|
|
539
|
+
this.sponsorBlockChaptersLoaded(player, track, payload);
|
|
488
540
|
break;
|
|
489
541
|
case "ChapterStarted":
|
|
490
|
-
this.sponsorBlockChapterStarted(player,
|
|
542
|
+
this.sponsorBlockChapterStarted(player, track, payload);
|
|
543
|
+
break;
|
|
544
|
+
case "LyricsFoundEvent":
|
|
545
|
+
this.lyricsFound(player, track, payload);
|
|
546
|
+
break;
|
|
547
|
+
case "LyricsNotFoundEvent":
|
|
548
|
+
this.lyricsNotFound(player, track, payload);
|
|
549
|
+
break;
|
|
550
|
+
case "LyricsLineEvent":
|
|
551
|
+
this.lyricsLine(player, track, payload);
|
|
491
552
|
break;
|
|
492
553
|
default:
|
|
493
554
|
error = new Error(`Node#event unknown event '${type}'.`);
|
|
494
|
-
this.manager.emit(
|
|
555
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
495
556
|
break;
|
|
496
557
|
}
|
|
497
558
|
}
|
|
@@ -506,22 +567,24 @@ class Node {
|
|
|
506
567
|
const oldPlayer = player;
|
|
507
568
|
player.playing = true;
|
|
508
569
|
player.paused = false;
|
|
509
|
-
this.manager.emit(
|
|
570
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackStart, player, track, payload);
|
|
510
571
|
const botUser = player.get("Internal_BotUser");
|
|
511
572
|
if (botUser && botUser.id === track.requester.id) {
|
|
512
|
-
this.manager.emit(
|
|
513
|
-
changeType:
|
|
573
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
574
|
+
changeType: Enums_1.PlayerStateEventTypes.TrackChange,
|
|
514
575
|
details: {
|
|
515
|
-
|
|
576
|
+
type: "track",
|
|
577
|
+
action: "autoPlay",
|
|
516
578
|
track: track,
|
|
517
579
|
},
|
|
518
580
|
});
|
|
519
581
|
return;
|
|
520
582
|
}
|
|
521
|
-
this.manager.emit(
|
|
522
|
-
changeType:
|
|
583
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
584
|
+
changeType: Enums_1.PlayerStateEventTypes.TrackChange,
|
|
523
585
|
details: {
|
|
524
|
-
|
|
586
|
+
type: "track",
|
|
587
|
+
action: "start",
|
|
525
588
|
track: track,
|
|
526
589
|
},
|
|
527
590
|
});
|
|
@@ -536,42 +599,41 @@ class Node {
|
|
|
536
599
|
async trackEnd(player, track, payload) {
|
|
537
600
|
const { reason } = payload;
|
|
538
601
|
const skipFlag = player.get("skipFlag");
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
602
|
+
const previous = await player.queue.getPrevious();
|
|
603
|
+
const current = await player.queue.getCurrent();
|
|
604
|
+
if (!skipFlag && (previous.length === 0 || (previous[0] && previous[0].track !== current?.track))) {
|
|
605
|
+
await player.queue.addPrevious(current);
|
|
606
|
+
const updated = await player.queue.getPrevious();
|
|
607
|
+
if (updated.length > this.manager.options.maxPreviousTracks) {
|
|
608
|
+
const trimmed = updated.slice(0, this.manager.options.maxPreviousTracks);
|
|
609
|
+
await player.queue.setPrevious(trimmed);
|
|
545
610
|
}
|
|
546
611
|
}
|
|
612
|
+
player.set("skipFlag", false);
|
|
547
613
|
const oldPlayer = player;
|
|
548
|
-
// Handle track end events
|
|
549
614
|
switch (reason) {
|
|
550
|
-
case
|
|
551
|
-
case
|
|
552
|
-
// Handle the case when a track failed to load or was cleaned up
|
|
615
|
+
case Enums_1.TrackEndReasonTypes.LoadFailed:
|
|
616
|
+
case Enums_1.TrackEndReasonTypes.Cleanup:
|
|
553
617
|
await this.handleFailedTrack(player, track, payload);
|
|
554
618
|
break;
|
|
555
|
-
case
|
|
556
|
-
// Handle the case when a track was replaced
|
|
619
|
+
case Enums_1.TrackEndReasonTypes.Replaced:
|
|
557
620
|
break;
|
|
558
|
-
case
|
|
559
|
-
|
|
560
|
-
if (player.queue.length) {
|
|
621
|
+
case Enums_1.TrackEndReasonTypes.Stopped:
|
|
622
|
+
if (await player.queue.size()) {
|
|
561
623
|
await this.playNextTrack(player, track, payload);
|
|
562
624
|
}
|
|
563
625
|
else {
|
|
564
626
|
await this.queueEnd(player, track, payload);
|
|
565
627
|
}
|
|
566
628
|
break;
|
|
567
|
-
case
|
|
629
|
+
case Enums_1.TrackEndReasonTypes.Finished:
|
|
568
630
|
// If the track ended and it's set to repeat (track or queue)
|
|
569
631
|
if (track && (player.trackRepeat || player.queueRepeat)) {
|
|
570
632
|
await this.handleRepeatedTrack(player, track, payload);
|
|
571
633
|
break;
|
|
572
634
|
}
|
|
573
635
|
// If there's another track in the queue
|
|
574
|
-
if (player.queue.
|
|
636
|
+
if (await player.queue.size()) {
|
|
575
637
|
await this.playNextTrack(player, track, payload);
|
|
576
638
|
}
|
|
577
639
|
else {
|
|
@@ -579,13 +641,14 @@ class Node {
|
|
|
579
641
|
}
|
|
580
642
|
break;
|
|
581
643
|
default:
|
|
582
|
-
this.manager.emit(
|
|
644
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Unexpected track end reason "${reason}"`));
|
|
583
645
|
break;
|
|
584
646
|
}
|
|
585
|
-
this.manager.emit(
|
|
586
|
-
changeType:
|
|
647
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
648
|
+
changeType: Enums_1.PlayerStateEventTypes.TrackChange,
|
|
587
649
|
details: {
|
|
588
|
-
|
|
650
|
+
type: "track",
|
|
651
|
+
action: "end",
|
|
589
652
|
track: track,
|
|
590
653
|
},
|
|
591
654
|
});
|
|
@@ -602,15 +665,22 @@ class Node {
|
|
|
602
665
|
*/
|
|
603
666
|
async handleAutoplay(player, attempt = 0) {
|
|
604
667
|
// If autoplay is not enabled or all attempts have failed, early exit
|
|
605
|
-
if (!player.isAutoplay || attempt > player.autoplayTries || !player.queue.
|
|
668
|
+
if (!player.isAutoplay || attempt > player.autoplayTries || !(await player.queue.getPrevious()).length)
|
|
606
669
|
return false;
|
|
607
|
-
const
|
|
670
|
+
const PreviousQueue = await player.queue.getPrevious();
|
|
671
|
+
const lastTrack = PreviousQueue?.at(-1);
|
|
608
672
|
lastTrack.requester = player.get("Internal_BotUser");
|
|
609
673
|
if (!lastTrack)
|
|
610
674
|
return false;
|
|
611
675
|
const tracks = await Utils_1.AutoPlayUtils.getRecommendedTracks(lastTrack);
|
|
612
|
-
|
|
613
|
-
|
|
676
|
+
const normalize = (str) => str
|
|
677
|
+
.toLowerCase()
|
|
678
|
+
.replace(/\s+/g, "")
|
|
679
|
+
.replace(/[^a-z0-9]/g, "");
|
|
680
|
+
const filteredTracks = tracks.filter((track) => track.identifier !== lastTrack.identifier && track.uri !== lastTrack.uri && normalize(track.title) !== normalize(lastTrack.title));
|
|
681
|
+
if (filteredTracks.length) {
|
|
682
|
+
const randomTrack = filteredTracks[Math.floor(Math.random() * filteredTracks.length)];
|
|
683
|
+
await player.queue.add(randomTrack);
|
|
614
684
|
await player.play();
|
|
615
685
|
return true;
|
|
616
686
|
}
|
|
@@ -631,12 +701,12 @@ class Node {
|
|
|
631
701
|
* @private
|
|
632
702
|
*/
|
|
633
703
|
async handleFailedTrack(player, track, payload) {
|
|
634
|
-
player.queue.
|
|
635
|
-
if (!player.queue.
|
|
704
|
+
await player.queue.setCurrent(await player.queue.dequeue());
|
|
705
|
+
if (!(await player.queue.getCurrent())) {
|
|
636
706
|
await this.queueEnd(player, track, payload);
|
|
637
707
|
return;
|
|
638
708
|
}
|
|
639
|
-
this.manager.emit(
|
|
709
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
640
710
|
if (this.manager.options.playNextOnEnd)
|
|
641
711
|
await player.play();
|
|
642
712
|
}
|
|
@@ -657,26 +727,27 @@ class Node {
|
|
|
657
727
|
const { playNextOnEnd } = this.manager.options;
|
|
658
728
|
if (trackRepeat) {
|
|
659
729
|
// Prevent duplicate repeat insertion
|
|
660
|
-
if (queue[0] !== queue.
|
|
661
|
-
queue.
|
|
730
|
+
if (queue[0] !== (await queue.getCurrent())) {
|
|
731
|
+
await queue.enqueueFront(await queue.getCurrent());
|
|
662
732
|
}
|
|
663
733
|
}
|
|
664
734
|
else if (queueRepeat) {
|
|
665
735
|
// Prevent duplicate queue insertion
|
|
666
|
-
if (queue[queue.
|
|
667
|
-
queue.add(queue.
|
|
736
|
+
if (queue[(await queue.size()) - 1] !== (await queue.getCurrent())) {
|
|
737
|
+
await queue.add(await queue.getCurrent());
|
|
668
738
|
}
|
|
669
739
|
}
|
|
670
740
|
// Move to the next track
|
|
671
|
-
queue.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
741
|
+
await queue.setCurrent(await queue.dequeue());
|
|
742
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
743
|
+
if (payload.reason === Enums_1.TrackEndReasonTypes.Stopped) {
|
|
744
|
+
const next = await queue.dequeue();
|
|
745
|
+
await queue.setCurrent(next ?? null);
|
|
746
|
+
if (!next) {
|
|
747
|
+
await this.queueEnd(player, track, payload);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
678
750
|
}
|
|
679
|
-
// If autoplay is enabled, play the next track
|
|
680
751
|
if (playNextOnEnd)
|
|
681
752
|
await player.play();
|
|
682
753
|
}
|
|
@@ -693,10 +764,8 @@ class Node {
|
|
|
693
764
|
*/
|
|
694
765
|
async playNextTrack(player, track, payload) {
|
|
695
766
|
// Shift the queue to set the next track as current
|
|
696
|
-
player.queue.
|
|
697
|
-
|
|
698
|
-
this.manager.emit(Manager_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
699
|
-
// If autoplay is enabled, play the next track
|
|
767
|
+
await player.queue.setCurrent(await player.queue.dequeue());
|
|
768
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
700
769
|
if (this.manager.options.playNextOnEnd)
|
|
701
770
|
await player.play();
|
|
702
771
|
}
|
|
@@ -710,10 +779,10 @@ class Node {
|
|
|
710
779
|
* @returns {Promise<void>} A promise that resolves when the queue end processing is complete.
|
|
711
780
|
*/
|
|
712
781
|
async queueEnd(player, track, payload) {
|
|
713
|
-
player.queue.
|
|
782
|
+
await player.queue.setCurrent(null);
|
|
714
783
|
if (!player.isAutoplay) {
|
|
715
784
|
player.playing = false;
|
|
716
|
-
this.manager.emit(
|
|
785
|
+
this.manager.emit(Enums_1.ManagerEventTypes.QueueEnd, player, track, payload);
|
|
717
786
|
return;
|
|
718
787
|
}
|
|
719
788
|
let attempt = 1;
|
|
@@ -724,24 +793,34 @@ class Node {
|
|
|
724
793
|
return;
|
|
725
794
|
attempt++;
|
|
726
795
|
}
|
|
727
|
-
// If all attempts fail, reset the player state and emit queueEnd
|
|
728
796
|
player.playing = false;
|
|
729
|
-
this.manager.emit(
|
|
797
|
+
this.manager.emit(Enums_1.ManagerEventTypes.QueueEnd, player, track, payload);
|
|
730
798
|
}
|
|
731
799
|
/**
|
|
732
800
|
* Fetches the lyrics of a track from the Lavalink node.
|
|
733
|
-
*
|
|
734
|
-
* If the
|
|
801
|
+
*
|
|
802
|
+
* If the node is a NodeLink, it will use the `NodeLinkGetLyrics` method to fetch the lyrics.
|
|
803
|
+
*
|
|
804
|
+
* Requires the `lavalyrics-plugin` to be present in the Lavalink node.
|
|
805
|
+
* Requires the `lavasrc-plugin` or `java-lyrics-plugin` to be present in the Lavalink node.
|
|
735
806
|
*
|
|
736
807
|
* @param {Track} track - The track to fetch the lyrics for.
|
|
737
808
|
* @param {boolean} [skipTrackSource=false] - Whether to skip using the track's source URL.
|
|
738
|
-
* @
|
|
809
|
+
* @param {string} [language="en"] - The language of the lyrics.
|
|
810
|
+
* @returns {Promise<Lyrics | NodeLinkGetLyrics>} A promise that resolves with the lyrics data.
|
|
739
811
|
*/
|
|
740
|
-
async getLyrics(track, skipTrackSource = false) {
|
|
741
|
-
if (!this.
|
|
742
|
-
throw new RangeError(`
|
|
743
|
-
|
|
744
|
-
|
|
812
|
+
async getLyrics(track, skipTrackSource = false, language) {
|
|
813
|
+
if (!this.connected)
|
|
814
|
+
throw new RangeError(`The node is not connected to the lavalink server: ${this.options.identifier}`);
|
|
815
|
+
if (this.isNodeLink) {
|
|
816
|
+
return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&language=${language}` : ""}`));
|
|
817
|
+
}
|
|
818
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavalyrics-plugin")) {
|
|
819
|
+
throw new RangeError(`The plugin "lavalyrics-plugin" must be present in the lavalink node: ${this.options.identifier}`);
|
|
820
|
+
}
|
|
821
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavasrc-plugin" || plugin.name === "java-lyrics-plugin")) {
|
|
822
|
+
throw new RangeError(`One of the following plugins must also be present in the lavalink node: "lavasrc-plugin" or "java-lyrics-plugin" (Node: ${this.options.identifier})`);
|
|
823
|
+
}
|
|
745
824
|
return ((await this.rest.get(`/v4/lyrics?track=${encodeURIComponent(track.track)}&skipTrackSource=${skipTrackSource}`)) || {
|
|
746
825
|
source: null,
|
|
747
826
|
provider: null,
|
|
@@ -750,6 +829,42 @@ class Node {
|
|
|
750
829
|
plugin: [],
|
|
751
830
|
});
|
|
752
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* Subscribes to lyrics for a player.
|
|
834
|
+
* @param {string} guildId - The ID of the guild to subscribe to lyrics for.
|
|
835
|
+
* @param {boolean} [skipTrackSource=false] - Whether to skip using the track's source URL.
|
|
836
|
+
* @returns {Promise<unknown>} A promise that resolves when the subscription is complete.
|
|
837
|
+
* @throws {RangeError} If the node is not connected to the lavalink server or if the java-lyrics-plugin is not available.
|
|
838
|
+
*/
|
|
839
|
+
async lyricsSubscribe(guildId, skipTrackSource = false) {
|
|
840
|
+
if (!this.connected)
|
|
841
|
+
throw new RangeError(`The node is not connected to the lavalink server: ${this.options.identifier}`);
|
|
842
|
+
if (this.isNodeLink)
|
|
843
|
+
throw new RangeError(`The node is a node link: ${this.options.identifier}`);
|
|
844
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavalyrics-plugin")) {
|
|
845
|
+
throw new RangeError(`The plugin "lavalyrics-plugin" must be present in the lavalink node: ${this.options.identifier}`);
|
|
846
|
+
}
|
|
847
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavasrc-plugin" || plugin.name === "java-lyrics-plugin")) {
|
|
848
|
+
throw new RangeError(`One of the following plugins must also be present in the lavalink node: "lavasrc-plugin" or "java-lyrics-plugin" (Node: ${this.options.identifier})`);
|
|
849
|
+
}
|
|
850
|
+
return await this.rest.post(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource}`, {});
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Unsubscribes from lyrics for a player.
|
|
854
|
+
* @param {string} guildId - The ID of the guild to unsubscribe from lyrics for.
|
|
855
|
+
* @returns {Promise<unknown>} A promise that resolves when the unsubscription is complete.
|
|
856
|
+
* @throws {RangeError} If the node is not connected to the lavalink server or if the java-lyrics-plugin is not available.
|
|
857
|
+
*/
|
|
858
|
+
async lyricsUnsubscribe(guildId) {
|
|
859
|
+
if (!this.connected)
|
|
860
|
+
throw new RangeError(`The node is not connected to the lavalink server: ${this.options.identifier}`);
|
|
861
|
+
if (this.isNodeLink)
|
|
862
|
+
throw new RangeError(`The node is a node link: ${this.options.identifier}`);
|
|
863
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "java-lyrics-plugin")) {
|
|
864
|
+
throw new RangeError(`there is no java-lyrics-plugin available in the lavalink node: ${this.options.identifier}`);
|
|
865
|
+
}
|
|
866
|
+
return await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe`);
|
|
867
|
+
}
|
|
753
868
|
/**
|
|
754
869
|
* Handles the event when a track becomes stuck during playback.
|
|
755
870
|
* Stops the current track and emits a `trackStuck` event.
|
|
@@ -762,7 +877,7 @@ class Node {
|
|
|
762
877
|
*/
|
|
763
878
|
async trackStuck(player, track, payload) {
|
|
764
879
|
await player.stop();
|
|
765
|
-
this.manager.emit(
|
|
880
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackStuck, player, track, payload);
|
|
766
881
|
}
|
|
767
882
|
/**
|
|
768
883
|
* Handles the event when a track has an error during playback.
|
|
@@ -776,7 +891,7 @@ class Node {
|
|
|
776
891
|
*/
|
|
777
892
|
async trackError(player, track, payload) {
|
|
778
893
|
await player.stop();
|
|
779
|
-
this.manager.emit(
|
|
894
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackError, player, track, payload);
|
|
780
895
|
}
|
|
781
896
|
/**
|
|
782
897
|
* Emitted when the WebSocket connection for a player closes.
|
|
@@ -785,8 +900,8 @@ class Node {
|
|
|
785
900
|
* @param {WebSocketClosedEvent} payload - The event payload containing additional data about the WebSocket close event.
|
|
786
901
|
*/
|
|
787
902
|
socketClosed(player, payload) {
|
|
788
|
-
this.manager.emit(
|
|
789
|
-
this.manager.emit(
|
|
903
|
+
this.manager.emit(Enums_1.ManagerEventTypes.SocketClosed, player, payload);
|
|
904
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${JSON.stringify(payload)}`);
|
|
790
905
|
}
|
|
791
906
|
/**
|
|
792
907
|
* Emitted when the segments for a track are loaded.
|
|
@@ -796,7 +911,7 @@ class Node {
|
|
|
796
911
|
* @param {SponsorBlockSegmentsLoaded} payload - The event payload containing additional data about the segments loaded event.
|
|
797
912
|
*/
|
|
798
913
|
sponsorBlockSegmentLoaded(player, track, payload) {
|
|
799
|
-
return this.manager.emit(
|
|
914
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.SegmentsLoaded, player, track, payload);
|
|
800
915
|
}
|
|
801
916
|
/**
|
|
802
917
|
* Emitted when a segment of a track is skipped using the sponsorblock plugin.
|
|
@@ -806,7 +921,7 @@ class Node {
|
|
|
806
921
|
* @param {SponsorBlockSegmentSkipped} payload - The event payload containing additional data about the segment skipped event.
|
|
807
922
|
*/
|
|
808
923
|
sponsorBlockSegmentSkipped(player, track, payload) {
|
|
809
|
-
return this.manager.emit(
|
|
924
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.SegmentSkipped, player, track, payload);
|
|
810
925
|
}
|
|
811
926
|
/**
|
|
812
927
|
* Emitted when chapters for a track are loaded using the sponsorblock plugin.
|
|
@@ -816,7 +931,7 @@ class Node {
|
|
|
816
931
|
* @param {SponsorBlockChaptersLoaded} payload - The event payload containing additional data about the chapters loaded event.
|
|
817
932
|
*/
|
|
818
933
|
sponsorBlockChaptersLoaded(player, track, payload) {
|
|
819
|
-
return this.manager.emit(
|
|
934
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.ChaptersLoaded, player, track, payload);
|
|
820
935
|
}
|
|
821
936
|
/**
|
|
822
937
|
* Emitted when a chapter of a track is started using the sponsorblock plugin.
|
|
@@ -826,7 +941,37 @@ class Node {
|
|
|
826
941
|
* @param {SponsorBlockChapterStarted} payload - The event payload containing additional data about the chapter started event.
|
|
827
942
|
*/
|
|
828
943
|
sponsorBlockChapterStarted(player, track, payload) {
|
|
829
|
-
return this.manager.emit(
|
|
944
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.ChapterStarted, player, track, payload);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Emitted when lyrics for a track are found.
|
|
948
|
+
* The payload of the event will contain the lyrics.
|
|
949
|
+
* @param {Player} player - The player associated with the lyrics.
|
|
950
|
+
* @param {Track} track - The track associated with the lyrics.
|
|
951
|
+
* @param {LyricsFoundEvent} payload - The event payload containing additional data about the lyrics found event.
|
|
952
|
+
*/
|
|
953
|
+
lyricsFound(player, track, payload) {
|
|
954
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.LyricsFound, player, track, payload);
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Emitted when lyrics for a track are not found.
|
|
958
|
+
* The payload of the event will contain the track.
|
|
959
|
+
* @param {Player} player - The player associated with the lyrics.
|
|
960
|
+
* @param {Track} track - The track associated with the lyrics.
|
|
961
|
+
* @param {LyricsNotFoundEvent} payload - The event payload containing additional data about the lyrics not found event.
|
|
962
|
+
*/
|
|
963
|
+
lyricsNotFound(player, track, payload) {
|
|
964
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.LyricsNotFound, player, track, payload);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Emitted when a line of lyrics for a track is received.
|
|
968
|
+
* The payload of the event will contain the lyrics line.
|
|
969
|
+
* @param {Player} player - The player associated with the lyrics line.
|
|
970
|
+
* @param {Track} track - The track associated with the lyrics line.
|
|
971
|
+
* @param {LyricsLineEvent} payload - The event payload containing additional data about the lyrics line event.
|
|
972
|
+
*/
|
|
973
|
+
lyricsLine(player, track, payload) {
|
|
974
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.LyricsLine, player, track, payload);
|
|
830
975
|
}
|
|
831
976
|
/**
|
|
832
977
|
* Fetches Lavalink node information.
|
|
@@ -860,7 +1005,7 @@ class Node {
|
|
|
860
1005
|
* player.setSponsorBlock([SponsorBlockSegment.Sponsor, SponsorBlockSegment.SelfPromo]);
|
|
861
1006
|
* ```
|
|
862
1007
|
*/
|
|
863
|
-
async setSponsorBlock(player, segments = [SponsorBlockSegment.Sponsor, SponsorBlockSegment.SelfPromo]) {
|
|
1008
|
+
async setSponsorBlock(player, segments = [Enums_1.SponsorBlockSegment.Sponsor, Enums_1.SponsorBlockSegment.SelfPromo]) {
|
|
864
1009
|
if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin"))
|
|
865
1010
|
throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.options.identifier}`);
|
|
866
1011
|
if (!segments.length)
|
|
@@ -893,7 +1038,7 @@ class Node {
|
|
|
893
1038
|
const message = "Please do NOT delete the magmastream/ folder as it is used to store player data for autoresume etc.";
|
|
894
1039
|
if (!fs_1.default.existsSync(readmeFilePath)) {
|
|
895
1040
|
fs_1.default.writeFileSync(readmeFilePath, message, "utf-8");
|
|
896
|
-
this.manager.emit(
|
|
1041
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Created README file at: ${readmeFilePath}`);
|
|
897
1042
|
}
|
|
898
1043
|
}
|
|
899
1044
|
}
|