magmastream 2.9.0-dev.9 → 2.9.1-dev.0
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 +15 -33
- package/dist/index.d.ts +2813 -1502
- package/dist/index.js +14 -1
- package/dist/statestorage/JsonQueue.js +443 -0
- package/dist/{structures/Queue.js → statestorage/MemoryQueue.js} +263 -184
- package/dist/{structures → statestorage}/RedisQueue.js +310 -164
- package/dist/structures/Enums.js +263 -0
- package/dist/structures/Filters.js +159 -137
- package/dist/structures/Manager.js +755 -444
- package/dist/structures/Node.js +361 -189
- package/dist/structures/Player.js +275 -112
- package/dist/structures/Plugin.js +4 -1
- package/dist/structures/Rest.js +12 -8
- package/dist/structures/Types.js +3 -0
- package/dist/structures/Utils.js +453 -414
- package/dist/utils/managerCheck.js +8 -8
- package/dist/utils/nodeCheck.js +5 -5
- package/dist/utils/playerCheck.js +3 -3
- 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 -15
- package/dist/storage/CollectionPlayerStore.js +0 -77
- package/dist/storage/RedisPlayerStore.js +0 -156
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,48 @@ 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.Memory:
|
|
94
|
+
case Enums_1.StateStorageType.JSON:
|
|
95
|
+
this.sessionIdsFilePath = path_1.default.join(process.cwd(), "magmastream", "sessionData", "sessionIds.json");
|
|
96
|
+
const configDir = path_1.default.dirname(this.sessionIdsFilePath);
|
|
97
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
98
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
this.createSessionIdsFile();
|
|
101
|
+
this.createReadmeFile();
|
|
102
|
+
break;
|
|
103
|
+
case Enums_1.StateStorageType.Redis:
|
|
104
|
+
this.redisPrefix = this.manager.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
105
|
+
? this.manager.options.stateStorage.redisConfig.prefix
|
|
106
|
+
: this.manager.options.stateStorage.redisConfig.prefix ?? "magmastream:";
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
102
109
|
}
|
|
103
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Checks if the Node is currently connected.
|
|
112
|
+
* This method returns true if the Node has an active WebSocket connection, indicating it is ready to receive and process commands.
|
|
113
|
+
*/
|
|
104
114
|
get connected() {
|
|
105
115
|
if (!this.socket)
|
|
106
116
|
return false;
|
|
107
117
|
return this.socket.readyState === ws_1.default.OPEN;
|
|
108
118
|
}
|
|
109
|
-
/** Returns the address for this node. */
|
|
119
|
+
/** Returns the full address for this node, including the host and port. */
|
|
110
120
|
get address() {
|
|
111
121
|
return `${this.options.host}:${this.options.port}`;
|
|
112
122
|
}
|
|
113
|
-
/** @hidden */
|
|
114
|
-
static init(manager) {
|
|
115
|
-
this._manager = manager;
|
|
116
|
-
}
|
|
117
123
|
/**
|
|
118
124
|
* Creates the sessionIds.json file if it doesn't exist. This file is used to
|
|
119
125
|
* store the session IDs for each node. The session IDs are used to identify
|
|
120
126
|
* the node when resuming a session.
|
|
121
127
|
*/
|
|
122
128
|
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");
|
|
129
|
+
if (!fs_1.default.existsSync(this.sessionIdsFilePath)) {
|
|
130
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Creating sessionId file at: ${this.sessionIdsFilePath}`);
|
|
131
|
+
fs_1.default.writeFileSync(this.sessionIdsFilePath, JSON.stringify({}), "utf-8");
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
/**
|
|
@@ -135,21 +139,49 @@ class Node {
|
|
|
135
139
|
* of the node identifier and cluster ID. This allows multiple clusters to
|
|
136
140
|
* be used with the same node identifier.
|
|
137
141
|
*/
|
|
138
|
-
loadSessionIds() {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
async loadSessionIds() {
|
|
143
|
+
switch (this.manager.options.stateStorage.type) {
|
|
144
|
+
case Enums_1.StateStorageType.Memory:
|
|
145
|
+
case Enums_1.StateStorageType.JSON: {
|
|
146
|
+
if (fs_1.default.existsSync(this.sessionIdsFilePath)) {
|
|
147
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from file: ${this.sessionIdsFilePath}`);
|
|
148
|
+
const sessionIdsData = fs_1.default.readFileSync(this.sessionIdsFilePath, "utf-8");
|
|
149
|
+
this.sessionIdsMap = new Map(Object.entries(JSON.parse(sessionIdsData)));
|
|
150
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
151
|
+
if (this.sessionIdsMap.has(compositeKey)) {
|
|
152
|
+
this.sessionId = this.sessionIdsMap.get(compositeKey);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
152
156
|
}
|
|
157
|
+
case Enums_1.StateStorageType.Redis:
|
|
158
|
+
const key = `${this.redisPrefix}node:sessionIds`;
|
|
159
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Loading sessionIds from Redis key: ${key}`);
|
|
160
|
+
const currentRaw = await this.manager.redis.get(key);
|
|
161
|
+
if (currentRaw) {
|
|
162
|
+
try {
|
|
163
|
+
const sessionIds = JSON.parse(currentRaw);
|
|
164
|
+
if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
|
|
165
|
+
throw new Error("[NODE] loadSessionIds invalid data type from Redis.");
|
|
166
|
+
}
|
|
167
|
+
this.sessionIdsMap = new Map(Object.entries(sessionIds));
|
|
168
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
169
|
+
if (this.sessionIdsMap.has(compositeKey)) {
|
|
170
|
+
this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
|
|
171
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Restored sessionId for ${compositeKey}: ${this.sessionId}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to parse Redis sessionIds: ${err.message}`);
|
|
176
|
+
this.sessionIdsMap = new Map();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] No sessionIds found in Redis — creating new key.`);
|
|
181
|
+
await this.manager.redis.set(key, JSON.stringify({}));
|
|
182
|
+
this.sessionIdsMap = new Map();
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
153
185
|
}
|
|
154
186
|
}
|
|
155
187
|
/**
|
|
@@ -163,15 +195,74 @@ class Node {
|
|
|
163
195
|
* of the node identifier and cluster ID. This allows multiple clusters to
|
|
164
196
|
* be used with the same node identifier.
|
|
165
197
|
*/
|
|
166
|
-
updateSessionId() {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
async updateSessionId() {
|
|
199
|
+
switch (this.manager.options.stateStorage.type) {
|
|
200
|
+
case Enums_1.StateStorageType.Memory:
|
|
201
|
+
case Enums_1.StateStorageType.JSON: {
|
|
202
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds to file: ${this.sessionIdsFilePath}`);
|
|
203
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
204
|
+
const filePath = this.sessionIdsFilePath;
|
|
205
|
+
let updated = false;
|
|
206
|
+
let retries = 3;
|
|
207
|
+
while (!updated && retries > 0) {
|
|
208
|
+
try {
|
|
209
|
+
let fileData = {};
|
|
210
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
211
|
+
try {
|
|
212
|
+
const raw = fs_1.default.readFileSync(filePath, "utf-8");
|
|
213
|
+
fileData = raw.trim() ? JSON.parse(raw) : {};
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to read/parse sessionIds.json: ${err.message}`);
|
|
217
|
+
fileData = {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
fileData[compositeKey] = this.sessionId;
|
|
221
|
+
const tmpPath = `${filePath}.tmp`;
|
|
222
|
+
fs_1.default.writeFileSync(tmpPath, JSON.stringify(fileData, null, 2), "utf-8");
|
|
223
|
+
fs_1.default.renameSync(tmpPath, filePath);
|
|
224
|
+
this.sessionIdsMap = new Map(Object.entries(fileData));
|
|
225
|
+
updated = true;
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
retries--;
|
|
229
|
+
if (retries === 0) {
|
|
230
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to update sessionIds after retries: ${err.message}`);
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case Enums_1.StateStorageType.Redis: {
|
|
239
|
+
const key = `${this.redisPrefix}node:sessionIds`;
|
|
240
|
+
const compositeKey = `${this.options.identifier}::${this.manager.options.clusterId}`;
|
|
241
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Updating sessionIds in Redis key: ${key}`);
|
|
242
|
+
const currentRaw = await this.manager.redis.get(key);
|
|
243
|
+
let sessionIds;
|
|
244
|
+
if (currentRaw) {
|
|
245
|
+
try {
|
|
246
|
+
sessionIds = JSON.parse(currentRaw);
|
|
247
|
+
if (typeof sessionIds !== "object" || Array.isArray(sessionIds)) {
|
|
248
|
+
throw new Error("Invalid data type in Redis");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Corrupted Redis sessionIds, reinitializing: ${err.message}`);
|
|
253
|
+
sessionIds = {};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Redis key not found — creating new sessionIds key.`);
|
|
258
|
+
sessionIds = {};
|
|
259
|
+
}
|
|
260
|
+
sessionIds[compositeKey] = this.sessionId;
|
|
261
|
+
this.sessionIdsMap = new Map(Object.entries(sessionIds));
|
|
262
|
+
await this.manager.redis.set(key, JSON.stringify(sessionIds));
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
175
266
|
}
|
|
176
267
|
/**
|
|
177
268
|
* Connects to the Node.
|
|
@@ -182,7 +273,8 @@ class Node {
|
|
|
182
273
|
* If the node has no session ID but the `enableSessionResumeOption` option is true, it will use the session ID
|
|
183
274
|
* stored in the sessionIds.json file if it exists.
|
|
184
275
|
*/
|
|
185
|
-
connect() {
|
|
276
|
+
async connect() {
|
|
277
|
+
await this.loadSessionIds();
|
|
186
278
|
if (this.connected)
|
|
187
279
|
return;
|
|
188
280
|
const headers = {
|
|
@@ -194,13 +286,14 @@ class Node {
|
|
|
194
286
|
if (this.sessionId) {
|
|
195
287
|
headers["Session-Id"] = this.sessionId;
|
|
196
288
|
}
|
|
197
|
-
else if (this.options.enableSessionResumeOption && sessionIdsMap.has(compositeKey)) {
|
|
198
|
-
this.sessionId = sessionIdsMap.get(compositeKey) || null;
|
|
289
|
+
else if (this.options.enableSessionResumeOption && this.sessionIdsMap.has(compositeKey)) {
|
|
290
|
+
this.sessionId = this.sessionIdsMap.get(compositeKey) || null;
|
|
199
291
|
headers["Session-Id"] = this.sessionId;
|
|
200
292
|
}
|
|
201
293
|
this.socket = new ws_1.default(`ws${this.options.useSSL ? "s" : ""}://${this.address}/v4/websocket`, { headers });
|
|
202
294
|
this.socket.on("open", this.open.bind(this));
|
|
203
295
|
this.socket.on("close", this.close.bind(this));
|
|
296
|
+
this.socket.on("upgrade", (request) => this.upgrade(request));
|
|
204
297
|
this.socket.on("message", this.message.bind(this));
|
|
205
298
|
this.socket.on("error", this.error.bind(this));
|
|
206
299
|
const debugInfo = {
|
|
@@ -214,7 +307,7 @@ class Node {
|
|
|
214
307
|
identifier: this.options.identifier,
|
|
215
308
|
},
|
|
216
309
|
};
|
|
217
|
-
this.manager.emit(
|
|
310
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connecting ${JSON.stringify(debugInfo)}`);
|
|
218
311
|
}
|
|
219
312
|
/**
|
|
220
313
|
* Destroys the node and cleans up associated resources.
|
|
@@ -229,33 +322,27 @@ class Node {
|
|
|
229
322
|
async destroy() {
|
|
230
323
|
if (!this.connected)
|
|
231
324
|
return;
|
|
232
|
-
// Emit a debug event indicating that the node is being destroyed
|
|
233
325
|
const debugInfo = {
|
|
234
326
|
connected: this.connected,
|
|
235
327
|
identifier: this.options.identifier,
|
|
236
328
|
address: this.address,
|
|
237
329
|
sessionId: this.sessionId,
|
|
238
|
-
playerCount:
|
|
330
|
+
playerCount: this.manager.players.filter((p) => p.node == this).size,
|
|
239
331
|
};
|
|
240
|
-
this.manager.emit(
|
|
332
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Destroying node: ${JSON.stringify(debugInfo)}`);
|
|
241
333
|
// Automove all players connected to that node
|
|
242
|
-
const players =
|
|
334
|
+
const players = this.manager.players.filter((p) => p.node == this);
|
|
243
335
|
if (players.size) {
|
|
244
|
-
players.
|
|
336
|
+
for (const player of players.values()) {
|
|
245
337
|
await player.autoMoveNode();
|
|
246
|
-
}
|
|
338
|
+
}
|
|
247
339
|
}
|
|
248
|
-
// Close the WebSocket connection
|
|
249
340
|
this.socket.close(1000, "destroy");
|
|
250
|
-
// Remove all event listeners on the WebSocket
|
|
251
341
|
this.socket.removeAllListeners();
|
|
252
|
-
// Clear the reconnect timeout
|
|
253
342
|
this.reconnectAttempts = 1;
|
|
254
343
|
clearTimeout(this.reconnectTimeout);
|
|
255
|
-
|
|
256
|
-
this.manager.
|
|
257
|
-
// Destroy the node from the manager
|
|
258
|
-
await this.manager.destroyNode(this.options.identifier);
|
|
344
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeDestroy, this);
|
|
345
|
+
this.manager.nodes.delete(this.options.identifier);
|
|
259
346
|
}
|
|
260
347
|
/**
|
|
261
348
|
* Attempts to reconnect to the node if the connection is lost.
|
|
@@ -274,7 +361,6 @@ class Node {
|
|
|
274
361
|
* @emits {nodeDestroy} - Emits a nodeDestroy event if the maximum number of retry attempts is reached.
|
|
275
362
|
*/
|
|
276
363
|
async reconnect() {
|
|
277
|
-
// Collect debug information regarding the current state of the node
|
|
278
364
|
const debugInfo = {
|
|
279
365
|
identifier: this.options.identifier,
|
|
280
366
|
connected: this.connected,
|
|
@@ -282,27 +368,28 @@ class Node {
|
|
|
282
368
|
maxRetryAttempts: this.options.maxRetryAttempts,
|
|
283
369
|
retryDelayMs: this.options.retryDelayMs,
|
|
284
370
|
};
|
|
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
|
|
371
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Reconnecting node: ${JSON.stringify(debugInfo)}`);
|
|
288
372
|
this.reconnectTimeout = setTimeout(async () => {
|
|
289
|
-
// Check if the maximum number of retry attempts has been reached
|
|
290
373
|
if (this.reconnectAttempts >= this.options.maxRetryAttempts) {
|
|
291
|
-
// Emit an error event and destroy the node if retries are exhausted
|
|
292
374
|
const error = new Error(`Unable to connect after ${this.options.maxRetryAttempts} attempts.`);
|
|
293
|
-
this.manager.emit(
|
|
375
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
294
376
|
return await this.destroy();
|
|
295
377
|
}
|
|
296
|
-
// Remove all listeners from the current WebSocket and reset it
|
|
297
378
|
this.socket?.removeAllListeners();
|
|
298
379
|
this.socket = null;
|
|
299
|
-
|
|
300
|
-
this.manager.emit(Manager_1.ManagerEventTypes.NodeReconnect, this);
|
|
380
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeReconnect, this);
|
|
301
381
|
this.connect();
|
|
302
|
-
// Increment the reconnect attempts counter
|
|
303
382
|
this.reconnectAttempts++;
|
|
304
383
|
}, this.options.retryDelayMs);
|
|
305
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Upgrades the node to a NodeLink.
|
|
387
|
+
*
|
|
388
|
+
* @param request - The incoming message.
|
|
389
|
+
*/
|
|
390
|
+
upgrade(request) {
|
|
391
|
+
this.isNodeLink = this.options.isNodeLink ?? Boolean(request.headers.isnodelink) ?? false;
|
|
392
|
+
}
|
|
306
393
|
/**
|
|
307
394
|
* Handles the "open" event emitted by the WebSocket connection.
|
|
308
395
|
*
|
|
@@ -312,18 +399,18 @@ class Node {
|
|
|
312
399
|
* with the node as the argument.
|
|
313
400
|
*/
|
|
314
401
|
open() {
|
|
315
|
-
// Clear any existing reconnect timeouts
|
|
316
402
|
if (this.reconnectTimeout)
|
|
317
403
|
clearTimeout(this.reconnectTimeout);
|
|
318
|
-
// Collect debug information regarding the current state of the node
|
|
319
404
|
const debugInfo = {
|
|
320
405
|
identifier: this.options.identifier,
|
|
321
406
|
connected: this.connected,
|
|
322
407
|
};
|
|
323
|
-
|
|
324
|
-
this.manager.emit(
|
|
325
|
-
|
|
326
|
-
|
|
408
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Connected node: ${JSON.stringify(debugInfo)}`);
|
|
409
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeConnect, this);
|
|
410
|
+
const playersOnBackupNode = this.manager.players.filter((p) => p.node.options.isBackup);
|
|
411
|
+
if (playersOnBackupNode.size) {
|
|
412
|
+
Promise.all(Array.from(playersOnBackupNode.values(), (player) => player.moveNode(this.options.identifier)));
|
|
413
|
+
}
|
|
327
414
|
}
|
|
328
415
|
/**
|
|
329
416
|
* Handles the "close" event emitted by the WebSocket connection.
|
|
@@ -344,18 +431,21 @@ class Node {
|
|
|
344
431
|
code,
|
|
345
432
|
reason,
|
|
346
433
|
};
|
|
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
|
|
434
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeDisconnect, this, { code, reason });
|
|
435
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Disconnected node: ${JSON.stringify(debugInfo)}`);
|
|
352
436
|
if (this.manager.useableNode) {
|
|
353
|
-
const players =
|
|
437
|
+
const players = this.manager.players.filter((p) => p.node.options.identifier == this.options.identifier);
|
|
354
438
|
if (players.size) {
|
|
355
439
|
await Promise.all(Array.from(players.values(), (player) => player.autoMoveNode()));
|
|
356
440
|
}
|
|
357
441
|
}
|
|
358
|
-
|
|
442
|
+
else {
|
|
443
|
+
const backUpNodes = this.manager.nodes.filter((node) => node.options.isBackup && node.connected);
|
|
444
|
+
const backupNode = backUpNodes.first();
|
|
445
|
+
if (backupNode) {
|
|
446
|
+
await Promise.all(Array.from(this.manager.players.values(), (player) => player.moveNode(backupNode.options.identifier)));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
359
449
|
if (code !== 1000 || reason !== "destroy")
|
|
360
450
|
await this.reconnect();
|
|
361
451
|
}
|
|
@@ -370,15 +460,12 @@ class Node {
|
|
|
370
460
|
error(error) {
|
|
371
461
|
if (!error)
|
|
372
462
|
return;
|
|
373
|
-
// Collect debug information regarding the error
|
|
374
463
|
const debugInfo = {
|
|
375
464
|
identifier: this.options.identifier,
|
|
376
465
|
error: error.message,
|
|
377
466
|
};
|
|
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);
|
|
467
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Error on node: ${JSON.stringify(debugInfo)}`);
|
|
468
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
382
469
|
}
|
|
383
470
|
/**
|
|
384
471
|
* Handles incoming messages from the Lavalink WebSocket connection.
|
|
@@ -397,7 +484,7 @@ class Node {
|
|
|
397
484
|
const payload = JSON.parse(d.toString());
|
|
398
485
|
if (!payload.op)
|
|
399
486
|
return;
|
|
400
|
-
this.manager.emit(
|
|
487
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeRaw, payload);
|
|
401
488
|
let player;
|
|
402
489
|
switch (payload.op) {
|
|
403
490
|
case "stats":
|
|
@@ -405,7 +492,7 @@ class Node {
|
|
|
405
492
|
this.stats = { ...payload };
|
|
406
493
|
break;
|
|
407
494
|
case "playerUpdate":
|
|
408
|
-
player =
|
|
495
|
+
player = this.manager.players.get(payload.guildId);
|
|
409
496
|
if (player && player.node.options.identifier !== this.options.identifier) {
|
|
410
497
|
return;
|
|
411
498
|
}
|
|
@@ -413,33 +500,31 @@ class Node {
|
|
|
413
500
|
player.position = payload.state.position || 0;
|
|
414
501
|
break;
|
|
415
502
|
case "event":
|
|
416
|
-
player =
|
|
503
|
+
player = this.manager.players.get(payload.guildId);
|
|
417
504
|
if (player && player.node.options.identifier !== this.options.identifier) {
|
|
418
505
|
return;
|
|
419
506
|
}
|
|
420
|
-
this.manager.emit(
|
|
507
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${JSON.stringify(payload)}`);
|
|
421
508
|
await this.handleEvent(payload);
|
|
422
509
|
break;
|
|
423
510
|
case "ready":
|
|
424
|
-
this.manager.emit(
|
|
511
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${JSON.stringify(payload)}`);
|
|
425
512
|
this.rest.setSessionId(payload.sessionId);
|
|
426
513
|
this.sessionId = payload.sessionId;
|
|
427
|
-
this.updateSessionId();
|
|
514
|
+
await this.updateSessionId();
|
|
428
515
|
this.info = await this.fetchInfo();
|
|
429
|
-
// Log if the session was resumed successfully
|
|
430
516
|
if (payload.resumed) {
|
|
431
|
-
// Load player states from the JSON file
|
|
432
517
|
await this.manager.loadPlayerStates(this.options.identifier);
|
|
433
518
|
}
|
|
434
519
|
if (this.options.enableSessionResumeOption) {
|
|
435
520
|
await this.rest.patch(`/v4/sessions/${this.sessionId}`, {
|
|
436
521
|
resuming: this.options.enableSessionResumeOption,
|
|
437
|
-
timeout: this.options.
|
|
522
|
+
timeout: this.options.sessionTimeoutSeconds,
|
|
438
523
|
});
|
|
439
524
|
}
|
|
440
525
|
break;
|
|
441
526
|
default:
|
|
442
|
-
this.manager.emit(
|
|
527
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Unexpected op "${payload.op}" with data: ${payload.message}`));
|
|
443
528
|
return;
|
|
444
529
|
}
|
|
445
530
|
}
|
|
@@ -452,7 +537,7 @@ class Node {
|
|
|
452
537
|
async handleEvent(payload) {
|
|
453
538
|
if (!payload.guildId)
|
|
454
539
|
return;
|
|
455
|
-
const player =
|
|
540
|
+
const player = this.manager.players.get(payload.guildId);
|
|
456
541
|
if (!player)
|
|
457
542
|
return;
|
|
458
543
|
const track = await player.queue.getCurrent();
|
|
@@ -489,9 +574,18 @@ class Node {
|
|
|
489
574
|
case "ChapterStarted":
|
|
490
575
|
this.sponsorBlockChapterStarted(player, track, payload);
|
|
491
576
|
break;
|
|
577
|
+
case "LyricsFoundEvent":
|
|
578
|
+
this.lyricsFound(player, track, payload);
|
|
579
|
+
break;
|
|
580
|
+
case "LyricsNotFoundEvent":
|
|
581
|
+
this.lyricsNotFound(player, track, payload);
|
|
582
|
+
break;
|
|
583
|
+
case "LyricsLineEvent":
|
|
584
|
+
this.lyricsLine(player, track, payload);
|
|
585
|
+
break;
|
|
492
586
|
default:
|
|
493
587
|
error = new Error(`Node#event unknown event '${type}'.`);
|
|
494
|
-
this.manager.emit(
|
|
588
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
495
589
|
break;
|
|
496
590
|
}
|
|
497
591
|
}
|
|
@@ -506,22 +600,24 @@ class Node {
|
|
|
506
600
|
const oldPlayer = player;
|
|
507
601
|
player.playing = true;
|
|
508
602
|
player.paused = false;
|
|
509
|
-
this.manager.emit(
|
|
603
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackStart, player, track, payload);
|
|
510
604
|
const botUser = player.get("Internal_BotUser");
|
|
511
605
|
if (botUser && botUser.id === track.requester.id) {
|
|
512
|
-
this.manager.emit(
|
|
513
|
-
changeType:
|
|
606
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
607
|
+
changeType: Enums_1.PlayerStateEventTypes.TrackChange,
|
|
514
608
|
details: {
|
|
515
|
-
|
|
609
|
+
type: "track",
|
|
610
|
+
action: "autoPlay",
|
|
516
611
|
track: track,
|
|
517
612
|
},
|
|
518
613
|
});
|
|
519
614
|
return;
|
|
520
615
|
}
|
|
521
|
-
this.manager.emit(
|
|
522
|
-
changeType:
|
|
616
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
617
|
+
changeType: Enums_1.PlayerStateEventTypes.TrackChange,
|
|
523
618
|
details: {
|
|
524
|
-
|
|
619
|
+
type: "track",
|
|
620
|
+
action: "start",
|
|
525
621
|
track: track,
|
|
526
622
|
},
|
|
527
623
|
});
|
|
@@ -539,26 +635,23 @@ class Node {
|
|
|
539
635
|
const previous = await player.queue.getPrevious();
|
|
540
636
|
const current = await player.queue.getCurrent();
|
|
541
637
|
if (!skipFlag && (previous.length === 0 || (previous[0] && previous[0].track !== current?.track))) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
638
|
+
await player.queue.addPrevious(current);
|
|
639
|
+
const updated = await player.queue.getPrevious();
|
|
640
|
+
if (updated.length > this.manager.options.maxPreviousTracks) {
|
|
641
|
+
const trimmed = updated.slice(0, this.manager.options.maxPreviousTracks);
|
|
642
|
+
await player.queue.setPrevious(trimmed);
|
|
547
643
|
}
|
|
548
644
|
}
|
|
645
|
+
player.set("skipFlag", false);
|
|
549
646
|
const oldPlayer = player;
|
|
550
|
-
// Handle track end events
|
|
551
647
|
switch (reason) {
|
|
552
|
-
case
|
|
553
|
-
case
|
|
554
|
-
// Handle the case when a track failed to load or was cleaned up
|
|
648
|
+
case Enums_1.TrackEndReasonTypes.LoadFailed:
|
|
649
|
+
case Enums_1.TrackEndReasonTypes.Cleanup:
|
|
555
650
|
await this.handleFailedTrack(player, track, payload);
|
|
556
651
|
break;
|
|
557
|
-
case
|
|
558
|
-
// Handle the case when a track was replaced
|
|
652
|
+
case Enums_1.TrackEndReasonTypes.Replaced:
|
|
559
653
|
break;
|
|
560
|
-
case
|
|
561
|
-
// If the track was forcibly replaced
|
|
654
|
+
case Enums_1.TrackEndReasonTypes.Stopped:
|
|
562
655
|
if (await player.queue.size()) {
|
|
563
656
|
await this.playNextTrack(player, track, payload);
|
|
564
657
|
}
|
|
@@ -566,7 +659,7 @@ class Node {
|
|
|
566
659
|
await this.queueEnd(player, track, payload);
|
|
567
660
|
}
|
|
568
661
|
break;
|
|
569
|
-
case
|
|
662
|
+
case Enums_1.TrackEndReasonTypes.Finished:
|
|
570
663
|
// If the track ended and it's set to repeat (track or queue)
|
|
571
664
|
if (track && (player.trackRepeat || player.queueRepeat)) {
|
|
572
665
|
await this.handleRepeatedTrack(player, track, payload);
|
|
@@ -581,13 +674,14 @@ class Node {
|
|
|
581
674
|
}
|
|
582
675
|
break;
|
|
583
676
|
default:
|
|
584
|
-
this.manager.emit(
|
|
677
|
+
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Unexpected track end reason "${reason}"`));
|
|
585
678
|
break;
|
|
586
679
|
}
|
|
587
|
-
this.manager.emit(
|
|
588
|
-
changeType:
|
|
680
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
681
|
+
changeType: Enums_1.PlayerStateEventTypes.TrackChange,
|
|
589
682
|
details: {
|
|
590
|
-
|
|
683
|
+
type: "track",
|
|
684
|
+
action: "end",
|
|
591
685
|
track: track,
|
|
592
686
|
},
|
|
593
687
|
});
|
|
@@ -606,13 +700,20 @@ class Node {
|
|
|
606
700
|
// If autoplay is not enabled or all attempts have failed, early exit
|
|
607
701
|
if (!player.isAutoplay || attempt > player.autoplayTries || !(await player.queue.getPrevious()).length)
|
|
608
702
|
return false;
|
|
609
|
-
const
|
|
703
|
+
const PreviousQueue = await player.queue.getPrevious();
|
|
704
|
+
const lastTrack = PreviousQueue?.at(-1);
|
|
610
705
|
lastTrack.requester = player.get("Internal_BotUser");
|
|
611
706
|
if (!lastTrack)
|
|
612
707
|
return false;
|
|
613
708
|
const tracks = await Utils_1.AutoPlayUtils.getRecommendedTracks(lastTrack);
|
|
614
|
-
|
|
615
|
-
|
|
709
|
+
const normalize = (str) => str
|
|
710
|
+
.toLowerCase()
|
|
711
|
+
.replace(/\s+/g, "")
|
|
712
|
+
.replace(/[^a-z0-9]/g, "");
|
|
713
|
+
const filteredTracks = tracks.filter((track) => track.identifier !== lastTrack.identifier && track.uri !== lastTrack.uri && normalize(track.title) !== normalize(lastTrack.title));
|
|
714
|
+
if (filteredTracks.length) {
|
|
715
|
+
const randomTrack = filteredTracks[Math.floor(Math.random() * filteredTracks.length)];
|
|
716
|
+
await player.queue.add(randomTrack);
|
|
616
717
|
await player.play();
|
|
617
718
|
return true;
|
|
618
719
|
}
|
|
@@ -638,7 +739,7 @@ class Node {
|
|
|
638
739
|
await this.queueEnd(player, track, payload);
|
|
639
740
|
return;
|
|
640
741
|
}
|
|
641
|
-
this.manager.emit(
|
|
742
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
642
743
|
if (this.manager.options.playNextOnEnd)
|
|
643
744
|
await player.play();
|
|
644
745
|
}
|
|
@@ -671,10 +772,8 @@ class Node {
|
|
|
671
772
|
}
|
|
672
773
|
// Move to the next track
|
|
673
774
|
await queue.setCurrent(await queue.dequeue());
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// If the track was stopped manually and no more tracks exist, end the queue
|
|
677
|
-
if (payload.reason === Utils_1.TrackEndReasonTypes.Stopped) {
|
|
775
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
776
|
+
if (payload.reason === Enums_1.TrackEndReasonTypes.Stopped) {
|
|
678
777
|
const next = await queue.dequeue();
|
|
679
778
|
await queue.setCurrent(next ?? null);
|
|
680
779
|
if (!next) {
|
|
@@ -682,7 +781,6 @@ class Node {
|
|
|
682
781
|
return;
|
|
683
782
|
}
|
|
684
783
|
}
|
|
685
|
-
// If autoplay is enabled, play the next track
|
|
686
784
|
if (playNextOnEnd)
|
|
687
785
|
await player.play();
|
|
688
786
|
}
|
|
@@ -700,9 +798,7 @@ class Node {
|
|
|
700
798
|
async playNextTrack(player, track, payload) {
|
|
701
799
|
// Shift the queue to set the next track as current
|
|
702
800
|
await player.queue.setCurrent(await player.queue.dequeue());
|
|
703
|
-
|
|
704
|
-
this.manager.emit(Manager_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
705
|
-
// If autoplay is enabled, play the next track
|
|
801
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
|
|
706
802
|
if (this.manager.options.playNextOnEnd)
|
|
707
803
|
await player.play();
|
|
708
804
|
}
|
|
@@ -719,7 +815,7 @@ class Node {
|
|
|
719
815
|
await player.queue.setCurrent(null);
|
|
720
816
|
if (!player.isAutoplay) {
|
|
721
817
|
player.playing = false;
|
|
722
|
-
this.manager.emit(
|
|
818
|
+
this.manager.emit(Enums_1.ManagerEventTypes.QueueEnd, player, track, payload);
|
|
723
819
|
return;
|
|
724
820
|
}
|
|
725
821
|
let attempt = 1;
|
|
@@ -730,24 +826,34 @@ class Node {
|
|
|
730
826
|
return;
|
|
731
827
|
attempt++;
|
|
732
828
|
}
|
|
733
|
-
// If all attempts fail, reset the player state and emit queueEnd
|
|
734
829
|
player.playing = false;
|
|
735
|
-
this.manager.emit(
|
|
830
|
+
this.manager.emit(Enums_1.ManagerEventTypes.QueueEnd, player, track, payload);
|
|
736
831
|
}
|
|
737
832
|
/**
|
|
738
833
|
* Fetches the lyrics of a track from the Lavalink node.
|
|
739
|
-
*
|
|
740
|
-
* If the
|
|
834
|
+
*
|
|
835
|
+
* If the node is a NodeLink, it will use the `NodeLinkGetLyrics` method to fetch the lyrics.
|
|
836
|
+
*
|
|
837
|
+
* Requires the `lavalyrics-plugin` to be present in the Lavalink node.
|
|
838
|
+
* Requires the `lavasrc-plugin` or `java-lyrics-plugin` to be present in the Lavalink node.
|
|
741
839
|
*
|
|
742
840
|
* @param {Track} track - The track to fetch the lyrics for.
|
|
743
841
|
* @param {boolean} [skipTrackSource=false] - Whether to skip using the track's source URL.
|
|
744
|
-
* @
|
|
842
|
+
* @param {string} [language="en"] - The language of the lyrics.
|
|
843
|
+
* @returns {Promise<Lyrics | NodeLinkGetLyrics>} A promise that resolves with the lyrics data.
|
|
745
844
|
*/
|
|
746
|
-
async getLyrics(track, skipTrackSource = false) {
|
|
747
|
-
if (!this.
|
|
748
|
-
throw new RangeError(`
|
|
749
|
-
|
|
750
|
-
|
|
845
|
+
async getLyrics(track, skipTrackSource = false, language) {
|
|
846
|
+
if (!this.connected)
|
|
847
|
+
throw new RangeError(`The node is not connected to the lavalink server: ${this.options.identifier}`);
|
|
848
|
+
if (this.isNodeLink) {
|
|
849
|
+
return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&language=${language}` : ""}`));
|
|
850
|
+
}
|
|
851
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavalyrics-plugin")) {
|
|
852
|
+
throw new RangeError(`The plugin "lavalyrics-plugin" must be present in the lavalink node: ${this.options.identifier}`);
|
|
853
|
+
}
|
|
854
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavasrc-plugin" || plugin.name === "java-lyrics-plugin")) {
|
|
855
|
+
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})`);
|
|
856
|
+
}
|
|
751
857
|
return ((await this.rest.get(`/v4/lyrics?track=${encodeURIComponent(track.track)}&skipTrackSource=${skipTrackSource}`)) || {
|
|
752
858
|
source: null,
|
|
753
859
|
provider: null,
|
|
@@ -756,6 +862,42 @@ class Node {
|
|
|
756
862
|
plugin: [],
|
|
757
863
|
});
|
|
758
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Subscribes to lyrics for a player.
|
|
867
|
+
* @param {string} guildId - The ID of the guild to subscribe to lyrics for.
|
|
868
|
+
* @param {boolean} [skipTrackSource=false] - Whether to skip using the track's source URL.
|
|
869
|
+
* @returns {Promise<unknown>} A promise that resolves when the subscription is complete.
|
|
870
|
+
* @throws {RangeError} If the node is not connected to the lavalink server or if the java-lyrics-plugin is not available.
|
|
871
|
+
*/
|
|
872
|
+
async lyricsSubscribe(guildId, skipTrackSource = false) {
|
|
873
|
+
if (!this.connected)
|
|
874
|
+
throw new RangeError(`The node is not connected to the lavalink server: ${this.options.identifier}`);
|
|
875
|
+
if (this.isNodeLink)
|
|
876
|
+
throw new RangeError(`The node is a node link: ${this.options.identifier}`);
|
|
877
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavalyrics-plugin")) {
|
|
878
|
+
throw new RangeError(`The plugin "lavalyrics-plugin" must be present in the lavalink node: ${this.options.identifier}`);
|
|
879
|
+
}
|
|
880
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "lavasrc-plugin" || plugin.name === "java-lyrics-plugin")) {
|
|
881
|
+
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})`);
|
|
882
|
+
}
|
|
883
|
+
return await this.rest.post(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource}`, {});
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Unsubscribes from lyrics for a player.
|
|
887
|
+
* @param {string} guildId - The ID of the guild to unsubscribe from lyrics for.
|
|
888
|
+
* @returns {Promise<unknown>} A promise that resolves when the unsubscription is complete.
|
|
889
|
+
* @throws {RangeError} If the node is not connected to the lavalink server or if the java-lyrics-plugin is not available.
|
|
890
|
+
*/
|
|
891
|
+
async lyricsUnsubscribe(guildId) {
|
|
892
|
+
if (!this.connected)
|
|
893
|
+
throw new RangeError(`The node is not connected to the lavalink server: ${this.options.identifier}`);
|
|
894
|
+
if (this.isNodeLink)
|
|
895
|
+
throw new RangeError(`The node is a node link: ${this.options.identifier}`);
|
|
896
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "java-lyrics-plugin")) {
|
|
897
|
+
throw new RangeError(`there is no java-lyrics-plugin available in the lavalink node: ${this.options.identifier}`);
|
|
898
|
+
}
|
|
899
|
+
return await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe`);
|
|
900
|
+
}
|
|
759
901
|
/**
|
|
760
902
|
* Handles the event when a track becomes stuck during playback.
|
|
761
903
|
* Stops the current track and emits a `trackStuck` event.
|
|
@@ -768,7 +910,7 @@ class Node {
|
|
|
768
910
|
*/
|
|
769
911
|
async trackStuck(player, track, payload) {
|
|
770
912
|
await player.stop();
|
|
771
|
-
this.manager.emit(
|
|
913
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackStuck, player, track, payload);
|
|
772
914
|
}
|
|
773
915
|
/**
|
|
774
916
|
* Handles the event when a track has an error during playback.
|
|
@@ -782,7 +924,7 @@ class Node {
|
|
|
782
924
|
*/
|
|
783
925
|
async trackError(player, track, payload) {
|
|
784
926
|
await player.stop();
|
|
785
|
-
this.manager.emit(
|
|
927
|
+
this.manager.emit(Enums_1.ManagerEventTypes.TrackError, player, track, payload);
|
|
786
928
|
}
|
|
787
929
|
/**
|
|
788
930
|
* Emitted when the WebSocket connection for a player closes.
|
|
@@ -791,8 +933,8 @@ class Node {
|
|
|
791
933
|
* @param {WebSocketClosedEvent} payload - The event payload containing additional data about the WebSocket close event.
|
|
792
934
|
*/
|
|
793
935
|
socketClosed(player, payload) {
|
|
794
|
-
this.manager.emit(
|
|
795
|
-
this.manager.emit(
|
|
936
|
+
this.manager.emit(Enums_1.ManagerEventTypes.SocketClosed, player, payload);
|
|
937
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${JSON.stringify(payload)}`);
|
|
796
938
|
}
|
|
797
939
|
/**
|
|
798
940
|
* Emitted when the segments for a track are loaded.
|
|
@@ -802,7 +944,7 @@ class Node {
|
|
|
802
944
|
* @param {SponsorBlockSegmentsLoaded} payload - The event payload containing additional data about the segments loaded event.
|
|
803
945
|
*/
|
|
804
946
|
sponsorBlockSegmentLoaded(player, track, payload) {
|
|
805
|
-
return this.manager.emit(
|
|
947
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.SegmentsLoaded, player, track, payload);
|
|
806
948
|
}
|
|
807
949
|
/**
|
|
808
950
|
* Emitted when a segment of a track is skipped using the sponsorblock plugin.
|
|
@@ -812,7 +954,7 @@ class Node {
|
|
|
812
954
|
* @param {SponsorBlockSegmentSkipped} payload - The event payload containing additional data about the segment skipped event.
|
|
813
955
|
*/
|
|
814
956
|
sponsorBlockSegmentSkipped(player, track, payload) {
|
|
815
|
-
return this.manager.emit(
|
|
957
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.SegmentSkipped, player, track, payload);
|
|
816
958
|
}
|
|
817
959
|
/**
|
|
818
960
|
* Emitted when chapters for a track are loaded using the sponsorblock plugin.
|
|
@@ -822,7 +964,7 @@ class Node {
|
|
|
822
964
|
* @param {SponsorBlockChaptersLoaded} payload - The event payload containing additional data about the chapters loaded event.
|
|
823
965
|
*/
|
|
824
966
|
sponsorBlockChaptersLoaded(player, track, payload) {
|
|
825
|
-
return this.manager.emit(
|
|
967
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.ChaptersLoaded, player, track, payload);
|
|
826
968
|
}
|
|
827
969
|
/**
|
|
828
970
|
* Emitted when a chapter of a track is started using the sponsorblock plugin.
|
|
@@ -832,7 +974,37 @@ class Node {
|
|
|
832
974
|
* @param {SponsorBlockChapterStarted} payload - The event payload containing additional data about the chapter started event.
|
|
833
975
|
*/
|
|
834
976
|
sponsorBlockChapterStarted(player, track, payload) {
|
|
835
|
-
return this.manager.emit(
|
|
977
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.ChapterStarted, player, track, payload);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Emitted when lyrics for a track are found.
|
|
981
|
+
* The payload of the event will contain the lyrics.
|
|
982
|
+
* @param {Player} player - The player associated with the lyrics.
|
|
983
|
+
* @param {Track} track - The track associated with the lyrics.
|
|
984
|
+
* @param {LyricsFoundEvent} payload - The event payload containing additional data about the lyrics found event.
|
|
985
|
+
*/
|
|
986
|
+
lyricsFound(player, track, payload) {
|
|
987
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.LyricsFound, player, track, payload);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Emitted when lyrics for a track are not found.
|
|
991
|
+
* The payload of the event will contain the track.
|
|
992
|
+
* @param {Player} player - The player associated with the lyrics.
|
|
993
|
+
* @param {Track} track - The track associated with the lyrics.
|
|
994
|
+
* @param {LyricsNotFoundEvent} payload - The event payload containing additional data about the lyrics not found event.
|
|
995
|
+
*/
|
|
996
|
+
lyricsNotFound(player, track, payload) {
|
|
997
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.LyricsNotFound, player, track, payload);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Emitted when a line of lyrics for a track is received.
|
|
1001
|
+
* The payload of the event will contain the lyrics line.
|
|
1002
|
+
* @param {Player} player - The player associated with the lyrics line.
|
|
1003
|
+
* @param {Track} track - The track associated with the lyrics line.
|
|
1004
|
+
* @param {LyricsLineEvent} payload - The event payload containing additional data about the lyrics line event.
|
|
1005
|
+
*/
|
|
1006
|
+
lyricsLine(player, track, payload) {
|
|
1007
|
+
return this.manager.emit(Enums_1.ManagerEventTypes.LyricsLine, player, track, payload);
|
|
836
1008
|
}
|
|
837
1009
|
/**
|
|
838
1010
|
* Fetches Lavalink node information.
|
|
@@ -866,7 +1038,7 @@ class Node {
|
|
|
866
1038
|
* player.setSponsorBlock([SponsorBlockSegment.Sponsor, SponsorBlockSegment.SelfPromo]);
|
|
867
1039
|
* ```
|
|
868
1040
|
*/
|
|
869
|
-
async setSponsorBlock(player, segments = [SponsorBlockSegment.Sponsor, SponsorBlockSegment.SelfPromo]) {
|
|
1041
|
+
async setSponsorBlock(player, segments = [Enums_1.SponsorBlockSegment.Sponsor, Enums_1.SponsorBlockSegment.SelfPromo]) {
|
|
870
1042
|
if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin"))
|
|
871
1043
|
throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.options.identifier}`);
|
|
872
1044
|
if (!segments.length)
|
|
@@ -899,7 +1071,7 @@ class Node {
|
|
|
899
1071
|
const message = "Please do NOT delete the magmastream/ folder as it is used to store player data for autoresume etc.";
|
|
900
1072
|
if (!fs_1.default.existsSync(readmeFilePath)) {
|
|
901
1073
|
fs_1.default.writeFileSync(readmeFilePath, message, "utf-8");
|
|
902
|
-
this.manager.emit(
|
|
1074
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Created README file at: ${readmeFilePath}`);
|
|
903
1075
|
}
|
|
904
1076
|
}
|
|
905
1077
|
}
|