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
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.Manager = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const Utils_1 = require("./Utils");
|
|
6
6
|
const collection_1 = require("@discordjs/collection");
|
|
7
7
|
const events_1 = require("events");
|
|
8
|
+
const Node_1 = require("./Node");
|
|
8
9
|
const __1 = require("..");
|
|
9
10
|
const managerCheck_1 = tslib_1.__importDefault(require("../utils/managerCheck"));
|
|
10
11
|
const blockedWords_1 = require("../config/blockedWords");
|
|
11
12
|
const promises_1 = tslib_1.__importDefault(require("fs/promises"));
|
|
12
13
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
13
14
|
const ioredis_1 = tslib_1.__importDefault(require("ioredis"));
|
|
15
|
+
const Enums_1 = require("./Enums");
|
|
16
|
+
const package_json_1 = require("../../package.json");
|
|
14
17
|
/**
|
|
15
|
-
* The main hub for interacting with Lavalink and using Magmastream
|
|
18
|
+
* The main hub for interacting with Lavalink and using Magmastream.
|
|
16
19
|
*/
|
|
17
20
|
class Manager extends events_1.EventEmitter {
|
|
18
21
|
/** The map of players. */
|
|
@@ -23,6 +26,8 @@ class Manager extends events_1.EventEmitter {
|
|
|
23
26
|
options;
|
|
24
27
|
initiated = false;
|
|
25
28
|
redis;
|
|
29
|
+
_send;
|
|
30
|
+
loadedPlugins = new Set();
|
|
26
31
|
/**
|
|
27
32
|
* Initiates the Manager class.
|
|
28
33
|
* @param options
|
|
@@ -41,36 +46,54 @@ class Manager extends events_1.EventEmitter {
|
|
|
41
46
|
constructor(options) {
|
|
42
47
|
super();
|
|
43
48
|
(0, managerCheck_1.default)(options);
|
|
49
|
+
// Initialize structures
|
|
44
50
|
Utils_1.Structure.get("Player").init(this);
|
|
45
|
-
Utils_1.Structure.get("Node").init(this);
|
|
46
51
|
Utils_1.TrackUtils.init(this);
|
|
47
|
-
Utils_1.
|
|
52
|
+
Utils_1.PlayerUtils.init(this);
|
|
48
53
|
if (options.trackPartial) {
|
|
49
54
|
Utils_1.TrackUtils.setTrackPartial(options.trackPartial);
|
|
50
55
|
delete options.trackPartial;
|
|
51
56
|
}
|
|
57
|
+
if (options.clientId)
|
|
58
|
+
this.options.clientId = options.clientId;
|
|
59
|
+
if (options.clusterId)
|
|
60
|
+
this.options.clusterId = options.clusterId;
|
|
61
|
+
if (options.send && !this._send)
|
|
62
|
+
this._send = options.send;
|
|
52
63
|
this.options = {
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
...options,
|
|
65
|
+
enabledPlugins: options.enabledPlugins ?? [],
|
|
66
|
+
nodes: options.nodes ?? [
|
|
55
67
|
{
|
|
56
|
-
identifier: "
|
|
57
|
-
host: "
|
|
68
|
+
identifier: "Cheap lavalink hosting @",
|
|
69
|
+
host: "https://blackforthosting.com/products?category=lavalink",
|
|
70
|
+
port: 443,
|
|
71
|
+
password: "Try BlackForHosting",
|
|
72
|
+
useSSL: true,
|
|
58
73
|
enableSessionResumeOption: false,
|
|
59
|
-
|
|
74
|
+
sessionTimeoutSeconds: 1000,
|
|
75
|
+
nodePriority: 69,
|
|
60
76
|
},
|
|
61
77
|
],
|
|
62
|
-
playNextOnEnd: true,
|
|
63
|
-
enablePriorityMode: false,
|
|
64
|
-
clientName:
|
|
65
|
-
defaultSearchPlatform: SearchPlatform.YouTube,
|
|
66
|
-
useNode: UseNodeOptions.LeastPlayers,
|
|
78
|
+
playNextOnEnd: options.playNextOnEnd ?? true,
|
|
79
|
+
enablePriorityMode: options.enablePriorityMode ?? false,
|
|
80
|
+
clientName: options.clientName ?? `Magmastream/${package_json_1.version}`,
|
|
81
|
+
defaultSearchPlatform: options.defaultSearchPlatform ?? Enums_1.SearchPlatform.YouTube,
|
|
82
|
+
useNode: options.useNode ?? Enums_1.UseNodeOptions.LeastPlayers,
|
|
67
83
|
maxPreviousTracks: options.maxPreviousTracks ?? 20,
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
normalizeYouTubeTitles: options.normalizeYouTubeTitles ?? false,
|
|
85
|
+
stateStorage: {
|
|
86
|
+
...options.stateStorage,
|
|
87
|
+
type: options.stateStorage?.type ?? Enums_1.StateStorageType.Memory,
|
|
88
|
+
deleteInactivePlayers: options.stateStorage?.deleteInactivePlayers ?? true,
|
|
89
|
+
},
|
|
90
|
+
autoPlaySearchPlatforms: options.autoPlaySearchPlatforms ?? [Enums_1.AutoPlayPlatform.YouTube],
|
|
91
|
+
send: this._send,
|
|
70
92
|
};
|
|
93
|
+
Utils_1.AutoPlayUtils.init(this);
|
|
71
94
|
if (this.options.nodes) {
|
|
72
95
|
for (const nodeOptions of this.options.nodes)
|
|
73
|
-
new
|
|
96
|
+
new Node_1.Node(this, nodeOptions);
|
|
74
97
|
}
|
|
75
98
|
process.on("SIGINT", async () => {
|
|
76
99
|
console.warn("\x1b[33mSIGINT received! Graceful shutdown initiated...\x1b[0m");
|
|
@@ -83,7 +106,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
83
106
|
}, 2000);
|
|
84
107
|
}
|
|
85
108
|
catch (error) {
|
|
86
|
-
console.error(
|
|
109
|
+
console.error(`[MANAGER] Error during shutdown: ${error}`);
|
|
87
110
|
process.exit(1);
|
|
88
111
|
}
|
|
89
112
|
});
|
|
@@ -95,46 +118,36 @@ class Manager extends events_1.EventEmitter {
|
|
|
95
118
|
process.exit(0);
|
|
96
119
|
}
|
|
97
120
|
catch (error) {
|
|
98
|
-
console.error(
|
|
121
|
+
console.error(`[MANAGER] Error during SIGTERM shutdown: ${error}`);
|
|
99
122
|
process.exit(1);
|
|
100
123
|
}
|
|
101
124
|
});
|
|
102
125
|
}
|
|
103
126
|
/**
|
|
104
127
|
* Initiates the Manager.
|
|
105
|
-
* @param clientId - The Discord client ID (required).
|
|
128
|
+
* @param clientId - The Discord client ID (only required when not using any of the magmastream wrappers).
|
|
106
129
|
* @param clusterId - The cluster ID which runs the current process (required).
|
|
107
130
|
* @returns The manager instance.
|
|
108
131
|
*/
|
|
109
|
-
init(
|
|
132
|
+
async init(options = {}) {
|
|
110
133
|
if (this.initiated) {
|
|
111
134
|
return this;
|
|
112
135
|
}
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
const { clientId, clusterId = 0 } = options;
|
|
137
|
+
if (clientId !== undefined) {
|
|
138
|
+
if (typeof clientId !== "string" || !/^\d+$/.test(clientId)) {
|
|
139
|
+
throw new Error('"clientId" must be a valid Discord client ID.');
|
|
140
|
+
}
|
|
141
|
+
this.options.clientId = clientId;
|
|
115
142
|
}
|
|
116
|
-
this.options.clientId = clientId;
|
|
117
143
|
if (typeof clusterId !== "number") {
|
|
118
|
-
console.warn(
|
|
119
|
-
clusterId = 0;
|
|
120
|
-
}
|
|
121
|
-
this.options.clusterId = clusterId;
|
|
122
|
-
for (const node of this.nodes.values()) {
|
|
123
|
-
try {
|
|
124
|
-
node.connect();
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
this.emit(ManagerEventTypes.NodeError, node, err);
|
|
128
|
-
}
|
|
144
|
+
console.warn(`[MANAGER] "clusterId" is not a valid number, defaulting to 0.`);
|
|
145
|
+
this.options.clusterId = 0;
|
|
129
146
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!(plugin instanceof __1.Plugin))
|
|
133
|
-
throw new RangeError(`Plugin at index ${index} does not extend Plugin.`);
|
|
134
|
-
plugin.load(this);
|
|
135
|
-
}
|
|
147
|
+
else {
|
|
148
|
+
this.options.clusterId = clusterId;
|
|
136
149
|
}
|
|
137
|
-
if (this.options.stateStorage
|
|
150
|
+
if (this.options.stateStorage.type === Enums_1.StateStorageType.Redis) {
|
|
138
151
|
const config = this.options.stateStorage.redisConfig;
|
|
139
152
|
this.redis = new ioredis_1.default({
|
|
140
153
|
host: config.host,
|
|
@@ -143,6 +156,15 @@ class Manager extends events_1.EventEmitter {
|
|
|
143
156
|
db: config.db ?? 0,
|
|
144
157
|
});
|
|
145
158
|
}
|
|
159
|
+
for (const node of this.nodes.values()) {
|
|
160
|
+
try {
|
|
161
|
+
await node.connect();
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
this.emit(Enums_1.ManagerEventTypes.NodeError, node, err);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.loadPlugins();
|
|
146
168
|
this.initiated = true;
|
|
147
169
|
return this;
|
|
148
170
|
}
|
|
@@ -159,7 +181,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
159
181
|
const _query = typeof query === "string" ? { query } : query;
|
|
160
182
|
const _source = _query.source ?? this.options.defaultSearchPlatform;
|
|
161
183
|
let search = /^https?:\/\//.test(_query.query) ? _query.query : `${_source}:${_query.query}`;
|
|
162
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Performing ${_source} search for: ${_query.query}`);
|
|
184
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Performing ${_source} search for: ${_query.query}`);
|
|
163
185
|
try {
|
|
164
186
|
const res = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`));
|
|
165
187
|
if (!res)
|
|
@@ -167,13 +189,19 @@ class Manager extends events_1.EventEmitter {
|
|
|
167
189
|
let tracks = [];
|
|
168
190
|
let playlist = null;
|
|
169
191
|
switch (res.loadType) {
|
|
170
|
-
case
|
|
192
|
+
case Enums_1.LoadTypes.Search:
|
|
171
193
|
tracks = res.data.map((track) => Utils_1.TrackUtils.build(track, requester));
|
|
172
194
|
break;
|
|
173
|
-
case
|
|
195
|
+
case Enums_1.LoadTypes.Short:
|
|
196
|
+
case Enums_1.LoadTypes.Track:
|
|
174
197
|
tracks = [Utils_1.TrackUtils.build(res.data, requester)];
|
|
175
198
|
break;
|
|
176
|
-
case
|
|
199
|
+
case Enums_1.LoadTypes.Album:
|
|
200
|
+
case Enums_1.LoadTypes.Artist:
|
|
201
|
+
case Enums_1.LoadTypes.Station:
|
|
202
|
+
case Enums_1.LoadTypes.Podcast:
|
|
203
|
+
case Enums_1.LoadTypes.Show:
|
|
204
|
+
case Enums_1.LoadTypes.Playlist: {
|
|
177
205
|
const playlistData = res.data;
|
|
178
206
|
tracks = playlistData.tracks.map((track) => Utils_1.TrackUtils.build(track, requester));
|
|
179
207
|
playlist = {
|
|
@@ -202,8 +230,27 @@ class Manager extends events_1.EventEmitter {
|
|
|
202
230
|
tracks = tracks.map(processTrack);
|
|
203
231
|
}
|
|
204
232
|
}
|
|
205
|
-
|
|
206
|
-
|
|
233
|
+
let result;
|
|
234
|
+
switch (res.loadType) {
|
|
235
|
+
case Enums_1.LoadTypes.Album:
|
|
236
|
+
case Enums_1.LoadTypes.Artist:
|
|
237
|
+
case Enums_1.LoadTypes.Station:
|
|
238
|
+
case Enums_1.LoadTypes.Podcast:
|
|
239
|
+
case Enums_1.LoadTypes.Show:
|
|
240
|
+
case Enums_1.LoadTypes.Playlist:
|
|
241
|
+
result = { loadType: res.loadType, tracks, playlist };
|
|
242
|
+
break;
|
|
243
|
+
case Enums_1.LoadTypes.Search:
|
|
244
|
+
result = { loadType: res.loadType, tracks };
|
|
245
|
+
break;
|
|
246
|
+
case Enums_1.LoadTypes.Short:
|
|
247
|
+
case Enums_1.LoadTypes.Track:
|
|
248
|
+
result = { loadType: res.loadType, tracks: [tracks[0]] };
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
return { loadType: res.loadType };
|
|
252
|
+
}
|
|
253
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Result ${_source} search for: ${_query.query}: ${JSON.stringify(result)}`);
|
|
207
254
|
return result;
|
|
208
255
|
}
|
|
209
256
|
catch (err) {
|
|
@@ -218,15 +265,6 @@ class Manager extends events_1.EventEmitter {
|
|
|
218
265
|
getPlayer(guildId) {
|
|
219
266
|
return this.players.get(guildId);
|
|
220
267
|
}
|
|
221
|
-
/**
|
|
222
|
-
* @deprecated - Will be removed with v2.10.0 use {@link getPlayer} instead
|
|
223
|
-
* Returns a player or undefined if it does not exist.
|
|
224
|
-
* @param guildId The guild ID of the player to retrieve.
|
|
225
|
-
* @returns The player if it exists, undefined otherwise.
|
|
226
|
-
*/
|
|
227
|
-
async get(guildId) {
|
|
228
|
-
return this.players.get(guildId);
|
|
229
|
-
}
|
|
230
268
|
/**
|
|
231
269
|
* Creates a player or returns one if it already exists.
|
|
232
270
|
* @param options The options to create the player with.
|
|
@@ -237,7 +275,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
237
275
|
return this.players.get(options.guildId);
|
|
238
276
|
}
|
|
239
277
|
// Create a new player with the given options
|
|
240
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Creating new player with options: ${JSON.stringify(options)}`);
|
|
278
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Creating new player with options: ${JSON.stringify(options)}`);
|
|
241
279
|
return new (Utils_1.Structure.get("Player"))(options);
|
|
242
280
|
}
|
|
243
281
|
/**
|
|
@@ -246,12 +284,11 @@ class Manager extends events_1.EventEmitter {
|
|
|
246
284
|
* @returns A promise that resolves when the player has been destroyed.
|
|
247
285
|
*/
|
|
248
286
|
async destroy(guildId) {
|
|
249
|
-
|
|
250
|
-
this.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
await this.cleanupInactivePlayers();
|
|
287
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Destroying player: ${guildId}`);
|
|
288
|
+
const player = this.getPlayer(guildId);
|
|
289
|
+
if (!player)
|
|
290
|
+
return;
|
|
291
|
+
await player.destroy();
|
|
255
292
|
}
|
|
256
293
|
/**
|
|
257
294
|
* Creates a new node or returns an existing one if it already exists.
|
|
@@ -259,15 +296,19 @@ class Manager extends events_1.EventEmitter {
|
|
|
259
296
|
* @returns The created node.
|
|
260
297
|
*/
|
|
261
298
|
createNode(options) {
|
|
299
|
+
const key = options.identifier || options.host;
|
|
262
300
|
// Check if the node already exists in the manager's collection
|
|
263
|
-
if (this.nodes.has(
|
|
301
|
+
if (this.nodes.has(key)) {
|
|
264
302
|
// Return the existing node if it does
|
|
265
|
-
return this.nodes.get(
|
|
303
|
+
return this.nodes.get(key);
|
|
266
304
|
}
|
|
305
|
+
const node = new Node_1.Node(this, options);
|
|
306
|
+
// Set the node in the manager's collection
|
|
307
|
+
this.nodes.set(key, node);
|
|
267
308
|
// Emit a debug event for node creation
|
|
268
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Creating new node with options: ${JSON.stringify(options)}`);
|
|
269
|
-
//
|
|
270
|
-
return
|
|
309
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Creating new node with options: ${JSON.stringify(options)}`);
|
|
310
|
+
// Return the created node
|
|
311
|
+
return node;
|
|
271
312
|
}
|
|
272
313
|
/**
|
|
273
314
|
* Destroys a node if it exists. Emits a debug event if the node is found and destroyed.
|
|
@@ -277,11 +318,13 @@ class Manager extends events_1.EventEmitter {
|
|
|
277
318
|
*/
|
|
278
319
|
async destroyNode(identifier) {
|
|
279
320
|
const node = this.nodes.get(identifier);
|
|
280
|
-
if (!node)
|
|
321
|
+
if (!node) {
|
|
322
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Tried to destroy non-existent node: ${identifier}`);
|
|
281
323
|
return;
|
|
282
|
-
|
|
283
|
-
|
|
324
|
+
}
|
|
325
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Destroying node: ${identifier}`);
|
|
284
326
|
this.nodes.delete(identifier);
|
|
327
|
+
await node.destroy();
|
|
285
328
|
}
|
|
286
329
|
/**
|
|
287
330
|
* Attaches an event listener to the manager.
|
|
@@ -307,18 +350,12 @@ class Manager extends events_1.EventEmitter {
|
|
|
307
350
|
const player = this.getPlayer(update.guild_id);
|
|
308
351
|
if (!player)
|
|
309
352
|
return;
|
|
310
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Updating voice state: ${JSON.stringify(update)}`);
|
|
353
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Updating voice state: ${JSON.stringify(update)}`);
|
|
311
354
|
if ("token" in update) {
|
|
312
355
|
return await this.handleVoiceServerUpdate(player, update);
|
|
313
356
|
}
|
|
314
357
|
if (update.user_id !== this.options.clientId)
|
|
315
358
|
return;
|
|
316
|
-
if (!player.voiceState.sessionId && player.voiceState.event) {
|
|
317
|
-
if (player.state !== Utils_1.StateTypes.Disconnected) {
|
|
318
|
-
await player.destroy();
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
359
|
return await this.handleVoiceStateUpdate(player, update);
|
|
323
360
|
}
|
|
324
361
|
/**
|
|
@@ -328,8 +365,8 @@ class Manager extends events_1.EventEmitter {
|
|
|
328
365
|
* @returns A promise that resolves to an array of TrackData objects.
|
|
329
366
|
* @throws Will throw an error if no nodes are available or if the API request fails.
|
|
330
367
|
*/
|
|
331
|
-
decodeTracks(tracks) {
|
|
332
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Decoding tracks: ${JSON.stringify(tracks)}`);
|
|
368
|
+
async decodeTracks(tracks) {
|
|
369
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Decoding tracks: ${JSON.stringify(tracks)}`);
|
|
333
370
|
return new Promise(async (resolve, reject) => {
|
|
334
371
|
const node = this.nodes.first();
|
|
335
372
|
if (!node)
|
|
@@ -353,201 +390,467 @@ class Manager extends events_1.EventEmitter {
|
|
|
353
390
|
return res[0];
|
|
354
391
|
}
|
|
355
392
|
/**
|
|
356
|
-
* Saves player states
|
|
393
|
+
* Saves player states.
|
|
357
394
|
* @param {string} guildId - The guild ID of the player to save
|
|
358
395
|
*/
|
|
359
396
|
async savePlayerState(guildId) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
397
|
+
switch (this.options.stateStorage.type) {
|
|
398
|
+
case Enums_1.StateStorageType.Memory:
|
|
399
|
+
case Enums_1.StateStorageType.JSON:
|
|
400
|
+
{
|
|
401
|
+
try {
|
|
402
|
+
const playerStateFilePath = Utils_1.PlayerUtils.getPlayerStatePath(guildId);
|
|
403
|
+
const player = this.getPlayer(guildId);
|
|
404
|
+
if (!player || player.state === Enums_1.StateTypes.Disconnected || !player.voiceChannelId) {
|
|
405
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Skipping save for inactive player: ${guildId}`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const serializedPlayer = await Utils_1.PlayerUtils.serializePlayer(player);
|
|
409
|
+
await promises_1.default.mkdir(path_1.default.dirname(playerStateFilePath), { recursive: true });
|
|
410
|
+
await promises_1.default.writeFile(playerStateFilePath, JSON.stringify(serializedPlayer, null, 2), "utf-8");
|
|
411
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Player state saved: ${guildId}`);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error saving player state for guild ${guildId}: ${error}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
break;
|
|
418
|
+
case Enums_1.StateStorageType.Redis:
|
|
419
|
+
{
|
|
420
|
+
try {
|
|
421
|
+
const player = this.getPlayer(guildId);
|
|
422
|
+
if (!player || player.state === Enums_1.StateTypes.Disconnected || !player.voiceChannelId) {
|
|
423
|
+
console.warn(`[MANAGER] Skipping save for inactive player: ${guildId}`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const serializedPlayer = await Utils_1.PlayerUtils.serializePlayer(player);
|
|
427
|
+
const redisKey = `${this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
428
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
429
|
+
: this.options.stateStorage.redisConfig.prefix ?? "magmastream:"}playerstore:${guildId}`;
|
|
430
|
+
await this.redis.set(redisKey, JSON.stringify(serializedPlayer));
|
|
431
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Player state saved to Redis: ${guildId}`);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error saving player state to Redis for guild ${guildId}: ${error}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
default:
|
|
365
439
|
return;
|
|
366
|
-
}
|
|
367
|
-
const serializedPlayer = this.serializePlayer(player);
|
|
368
|
-
await promises_1.default.writeFile(playerStateFilePath, JSON.stringify(serializedPlayer, null, 2), "utf-8");
|
|
369
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Player state saved: ${guildId}`);
|
|
370
|
-
}
|
|
371
|
-
catch (error) {
|
|
372
|
-
console.error(`Error saving player state for guild ${guildId}:`, error);
|
|
373
440
|
}
|
|
374
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Sleeps for a specified amount of time.
|
|
444
|
+
* @param ms The amount of time to sleep in milliseconds.
|
|
445
|
+
* @returns A promise that resolves after the specified amount of time.
|
|
446
|
+
*/
|
|
447
|
+
async sleep(ms) {
|
|
448
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
449
|
+
}
|
|
375
450
|
/**
|
|
376
451
|
* Loads player states from the JSON file.
|
|
377
452
|
* @param nodeId The ID of the node to load player states from.
|
|
378
453
|
* @returns A promise that resolves when the player states have been loaded.
|
|
379
454
|
*/
|
|
380
455
|
async loadPlayerStates(nodeId) {
|
|
381
|
-
this.emit(ManagerEventTypes.Debug, "[MANAGER] Loading saved players.");
|
|
456
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, "[MANAGER] Loading saved players.");
|
|
382
457
|
const node = this.nodes.get(nodeId);
|
|
383
458
|
if (!node)
|
|
384
459
|
throw new Error(`Could not find node: ${nodeId}`);
|
|
385
460
|
const info = (await node.rest.getAllPlayers());
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
for (const file of playerFiles) {
|
|
397
|
-
const filePath = path_1.default.join(playerStatesDir, file);
|
|
398
|
-
try {
|
|
399
|
-
// Check if the file exists (though readdir should only return valid files)
|
|
400
|
-
await promises_1.default.access(filePath);
|
|
401
|
-
// Read the file asynchronously
|
|
402
|
-
const data = await promises_1.default.readFile(filePath, "utf-8");
|
|
403
|
-
const state = JSON.parse(data);
|
|
404
|
-
if (state && typeof state === "object" && state.guildId && state.node.options.identifier === nodeId) {
|
|
405
|
-
const lavaPlayer = info.find((player) => player.guildId === state.guildId);
|
|
406
|
-
if (!lavaPlayer) {
|
|
407
|
-
await this.destroy(state.guildId);
|
|
408
|
-
}
|
|
409
|
-
const playerOptions = {
|
|
410
|
-
guildId: state.options.guildId,
|
|
411
|
-
textChannelId: state.options.textChannelId,
|
|
412
|
-
voiceChannelId: state.options.voiceChannelId,
|
|
413
|
-
selfDeafen: state.options.selfDeafen,
|
|
414
|
-
volume: lavaPlayer.volume || state.options.volume,
|
|
415
|
-
node: nodeId,
|
|
416
|
-
};
|
|
417
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Recreating player: ${state.guildId} from saved file: ${JSON.stringify(state.options)}`);
|
|
418
|
-
const player = this.create(playerOptions);
|
|
419
|
-
await player.node.rest.updatePlayer({
|
|
420
|
-
guildId: state.options.guildId,
|
|
421
|
-
data: { voice: { token: state.voiceState.event.token, endpoint: state.voiceState.event.endpoint, sessionId: state.voiceState.sessionId } },
|
|
461
|
+
switch (this.options.stateStorage.type) {
|
|
462
|
+
case Enums_1.StateStorageType.Memory:
|
|
463
|
+
case Enums_1.StateStorageType.JSON:
|
|
464
|
+
{
|
|
465
|
+
const playersBaseDir = Utils_1.PlayerUtils.getPlayersBaseDir();
|
|
466
|
+
try {
|
|
467
|
+
// Ensure base players directory exists
|
|
468
|
+
await promises_1.default.access(playersBaseDir).catch(async () => {
|
|
469
|
+
await promises_1.default.mkdir(playersBaseDir, { recursive: true });
|
|
470
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Created directory: ${playersBaseDir}`);
|
|
422
471
|
});
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
472
|
+
// Read guild directories inside players base dir
|
|
473
|
+
const guildDirs = await promises_1.default.readdir(playersBaseDir, { withFileTypes: true });
|
|
474
|
+
for (const dirent of guildDirs) {
|
|
475
|
+
if (!dirent.isDirectory())
|
|
476
|
+
continue;
|
|
477
|
+
const guildId = dirent.name;
|
|
478
|
+
const stateFilePath = Utils_1.PlayerUtils.getPlayerStatePath(guildId);
|
|
479
|
+
try {
|
|
480
|
+
await promises_1.default.access(stateFilePath);
|
|
481
|
+
const rawData = await promises_1.default.readFile(stateFilePath, "utf-8");
|
|
482
|
+
const state = JSON.parse(rawData);
|
|
483
|
+
if (state.clusterId !== this.options.clusterId)
|
|
484
|
+
continue;
|
|
485
|
+
if (!state.guildId || state.node?.options?.identifier !== nodeId)
|
|
486
|
+
continue;
|
|
487
|
+
const lavaPlayer = info.find((player) => player.guildId === state.guildId);
|
|
488
|
+
if (!lavaPlayer) {
|
|
489
|
+
await this.destroy(state.guildId);
|
|
490
|
+
continue;
|
|
432
491
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
492
|
+
const playerOptions = {
|
|
493
|
+
guildId: state.options.guildId,
|
|
494
|
+
textChannelId: state.options.textChannelId,
|
|
495
|
+
voiceChannelId: state.options.voiceChannelId,
|
|
496
|
+
selfDeafen: state.options.selfDeafen,
|
|
497
|
+
volume: lavaPlayer.volume || state.options.volume,
|
|
498
|
+
nodeIdentifier: nodeId,
|
|
499
|
+
};
|
|
500
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Recreating player: ${state.guildId} from saved file: ${JSON.stringify(state.options)}`);
|
|
501
|
+
const player = this.create(playerOptions);
|
|
502
|
+
await player.node.rest.updatePlayer({
|
|
503
|
+
guildId: state.options.guildId,
|
|
504
|
+
data: {
|
|
505
|
+
voice: {
|
|
506
|
+
token: state.voiceState.event.token,
|
|
507
|
+
endpoint: state.voiceState.event.endpoint,
|
|
508
|
+
sessionId: state.voiceState.sessionId,
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
player.connect();
|
|
513
|
+
const tracks = [];
|
|
514
|
+
const currentTrack = state.queue.current;
|
|
515
|
+
const queueTracks = state.queue.tracks;
|
|
516
|
+
if (state.isAutoplay) {
|
|
517
|
+
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
518
|
+
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
519
|
+
}
|
|
520
|
+
if (lavaPlayer?.track) {
|
|
521
|
+
tracks.push(...queueTracks);
|
|
522
|
+
if (currentTrack && currentTrack.uri === lavaPlayer.track.info.uri) {
|
|
523
|
+
await player.queue.setCurrent(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
|
|
524
|
+
}
|
|
525
|
+
if (tracks.length > 0) {
|
|
526
|
+
await player.queue.clear();
|
|
527
|
+
await player.queue.add(tracks);
|
|
528
|
+
}
|
|
440
529
|
}
|
|
441
530
|
else {
|
|
442
|
-
|
|
531
|
+
if (currentTrack) {
|
|
532
|
+
if (queueTracks.length > 0) {
|
|
533
|
+
tracks.push(...queueTracks);
|
|
534
|
+
await player.queue.clear();
|
|
535
|
+
await player.queue.add(tracks);
|
|
536
|
+
}
|
|
537
|
+
await node.trackEnd(player, currentTrack, {
|
|
538
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
539
|
+
type: "TrackEndEvent",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
const previousQueue = await player.queue.getPrevious();
|
|
544
|
+
const lastTrack = previousQueue?.at(-1);
|
|
545
|
+
if (lastTrack) {
|
|
546
|
+
if (queueTracks.length === 0) {
|
|
547
|
+
await node.trackEnd(player, lastTrack, {
|
|
548
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
549
|
+
type: "TrackEndEvent",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
tracks.push(...queueTracks);
|
|
554
|
+
if (tracks.length > 0) {
|
|
555
|
+
await player.queue.clear();
|
|
556
|
+
await player.queue.add(tracks);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
else if (queueTracks.length > 0) {
|
|
561
|
+
tracks.push(...queueTracks);
|
|
562
|
+
if (tracks.length > 0) {
|
|
563
|
+
await player.queue.clear();
|
|
564
|
+
await player.queue.add(tracks);
|
|
565
|
+
}
|
|
566
|
+
await node.trackEnd(player, lastTrack, {
|
|
567
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
568
|
+
type: "TrackEndEvent",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
443
572
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
573
|
+
if (state.queue.previous.length > 0) {
|
|
574
|
+
await player.queue.addPrevious(state.queue.previous);
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
await player.queue.clearPrevious();
|
|
578
|
+
}
|
|
579
|
+
if (state.paused) {
|
|
580
|
+
await player.pause(true);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
player.paused = false;
|
|
584
|
+
}
|
|
585
|
+
if (state.trackRepeat)
|
|
586
|
+
player.setTrackRepeat(true);
|
|
587
|
+
if (state.queueRepeat)
|
|
588
|
+
player.setQueueRepeat(true);
|
|
589
|
+
if (state.dynamicRepeat) {
|
|
590
|
+
player.setDynamicRepeat(state.dynamicRepeat, state.dynamicLoopInterval._idleTimeout);
|
|
591
|
+
}
|
|
592
|
+
if (state.data) {
|
|
593
|
+
for (const [name, value] of Object.entries(state.data)) {
|
|
594
|
+
player.set(name, value);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const filterActions = {
|
|
598
|
+
bassboost: () => player.filters.bassBoost(state.filters.bassBoostlevel),
|
|
599
|
+
distort: (enabled) => player.filters.distort(enabled),
|
|
600
|
+
setDistortion: () => player.filters.setDistortion(state.filters.distortion),
|
|
601
|
+
eightD: (enabled) => player.filters.eightD(enabled),
|
|
602
|
+
setKaraoke: () => player.filters.setKaraoke(state.filters.karaoke),
|
|
603
|
+
nightcore: (enabled) => player.filters.nightcore(enabled),
|
|
604
|
+
slowmo: (enabled) => player.filters.slowmo(enabled),
|
|
605
|
+
soft: (enabled) => player.filters.soft(enabled),
|
|
606
|
+
trebleBass: (enabled) => player.filters.trebleBass(enabled),
|
|
607
|
+
setTimescale: () => player.filters.setTimescale(state.filters.timescale),
|
|
608
|
+
tv: (enabled) => player.filters.tv(enabled),
|
|
609
|
+
vibrato: () => player.filters.setVibrato(state.filters.vibrato),
|
|
610
|
+
vaporwave: (enabled) => player.filters.vaporwave(enabled),
|
|
611
|
+
pop: (enabled) => player.filters.pop(enabled),
|
|
612
|
+
party: (enabled) => player.filters.party(enabled),
|
|
613
|
+
earrape: (enabled) => player.filters.earrape(enabled),
|
|
614
|
+
electronic: (enabled) => player.filters.electronic(enabled),
|
|
615
|
+
radio: (enabled) => player.filters.radio(enabled),
|
|
616
|
+
setRotation: () => player.filters.setRotation(state.filters.rotation),
|
|
617
|
+
tremolo: (enabled) => player.filters.tremolo(enabled),
|
|
618
|
+
china: (enabled) => player.filters.china(enabled),
|
|
619
|
+
chipmunk: (enabled) => player.filters.chipmunk(enabled),
|
|
620
|
+
darthvader: (enabled) => player.filters.darthvader(enabled),
|
|
621
|
+
daycore: (enabled) => player.filters.daycore(enabled),
|
|
622
|
+
doubletime: (enabled) => player.filters.doubletime(enabled),
|
|
623
|
+
demon: (enabled) => player.filters.demon(enabled),
|
|
450
624
|
};
|
|
451
|
-
|
|
625
|
+
for (const [filter, isEnabled] of Object.entries(state.filters.filterStatus)) {
|
|
626
|
+
if (isEnabled && filterActions[filter]) {
|
|
627
|
+
filterActions[filter](true);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
this.emit(Enums_1.ManagerEventTypes.PlayerRestored, player, node);
|
|
631
|
+
await this.sleep(1000);
|
|
452
632
|
}
|
|
453
|
-
|
|
454
|
-
|
|
633
|
+
catch (error) {
|
|
634
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing player state for guild ${guildId}: ${error}`);
|
|
635
|
+
continue;
|
|
455
636
|
}
|
|
456
637
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
if (state.trackRepeat)
|
|
473
|
-
player.setTrackRepeat(true);
|
|
474
|
-
if (state.queueRepeat)
|
|
475
|
-
player.setQueueRepeat(true);
|
|
476
|
-
if (state.dynamicRepeat) {
|
|
477
|
-
player.setDynamicRepeat(state.dynamicRepeat, state.dynamicLoopInterval._idleTimeout);
|
|
478
|
-
}
|
|
479
|
-
if (state.isAutoplay) {
|
|
480
|
-
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
481
|
-
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
482
|
-
}
|
|
483
|
-
if (state.data) {
|
|
484
|
-
for (const [name, value] of Object.entries(state.data)) {
|
|
485
|
-
player.set(name, value);
|
|
638
|
+
// Cleanup old player state files from guild directories whose nodeId matches
|
|
639
|
+
for (const dirent of guildDirs) {
|
|
640
|
+
if (!dirent.isDirectory())
|
|
641
|
+
continue;
|
|
642
|
+
const guildId = dirent.name;
|
|
643
|
+
const stateFilePath = Utils_1.PlayerUtils.getPlayerStatePath(guildId);
|
|
644
|
+
try {
|
|
645
|
+
await promises_1.default.access(stateFilePath);
|
|
646
|
+
const data = await promises_1.default.readFile(stateFilePath, "utf-8");
|
|
647
|
+
const state = JSON.parse(data);
|
|
648
|
+
if (state && typeof state === "object" && state.node?.options?.identifier === nodeId) {
|
|
649
|
+
await promises_1.default.rm(Utils_1.PlayerUtils.getPlayerStatePath(guildId), { force: true });
|
|
650
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted player state folder for guild ${guildId}`);
|
|
651
|
+
}
|
|
486
652
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
distort: (enabled) => player.filters.distort(enabled),
|
|
491
|
-
setDistortion: () => player.filters.setDistortion(state.filters.distortion),
|
|
492
|
-
eightD: (enabled) => player.filters.eightD(enabled),
|
|
493
|
-
setKaraoke: () => player.filters.setKaraoke(state.filters.karaoke),
|
|
494
|
-
nightcore: (enabled) => player.filters.nightcore(enabled),
|
|
495
|
-
slowmo: (enabled) => player.filters.slowmo(enabled),
|
|
496
|
-
soft: (enabled) => player.filters.soft(enabled),
|
|
497
|
-
trebleBass: (enabled) => player.filters.trebleBass(enabled),
|
|
498
|
-
setTimescale: () => player.filters.setTimescale(state.filters.timescale),
|
|
499
|
-
tv: (enabled) => player.filters.tv(enabled),
|
|
500
|
-
vibrato: () => player.filters.setVibrato(state.filters.vibrato),
|
|
501
|
-
vaporwave: (enabled) => player.filters.vaporwave(enabled),
|
|
502
|
-
pop: (enabled) => player.filters.pop(enabled),
|
|
503
|
-
party: (enabled) => player.filters.party(enabled),
|
|
504
|
-
earrape: (enabled) => player.filters.earrape(enabled),
|
|
505
|
-
electronic: (enabled) => player.filters.electronic(enabled),
|
|
506
|
-
radio: (enabled) => player.filters.radio(enabled),
|
|
507
|
-
setRotation: () => player.filters.setRotation(state.filters.rotation),
|
|
508
|
-
tremolo: (enabled) => player.filters.tremolo(enabled),
|
|
509
|
-
china: (enabled) => player.filters.china(enabled),
|
|
510
|
-
chipmunk: (enabled) => player.filters.chipmunk(enabled),
|
|
511
|
-
darthvader: (enabled) => player.filters.darthvader(enabled),
|
|
512
|
-
daycore: (enabled) => player.filters.daycore(enabled),
|
|
513
|
-
doubletime: (enabled) => player.filters.doubletime(enabled),
|
|
514
|
-
demon: (enabled) => player.filters.demon(enabled),
|
|
515
|
-
};
|
|
516
|
-
// Iterate through filterStatus and apply the enabled filters
|
|
517
|
-
for (const [filter, isEnabled] of Object.entries(state.filters.filterStatus)) {
|
|
518
|
-
if (isEnabled && filterActions[filter]) {
|
|
519
|
-
filterActions[filter](true);
|
|
653
|
+
catch (error) {
|
|
654
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error deleting player state for guild ${guildId}: ${error}`);
|
|
655
|
+
continue;
|
|
520
656
|
}
|
|
521
657
|
}
|
|
522
658
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Error processing file ${filePath}: ${error}`);
|
|
526
|
-
continue; // Skip to the next file if there's an error
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
// Delete all files inside playerStatesDir where nodeId matches
|
|
530
|
-
for (const file of playerFiles) {
|
|
531
|
-
const filePath = path_1.default.join(playerStatesDir, file);
|
|
532
|
-
try {
|
|
533
|
-
await promises_1.default.access(filePath); // Check if the file exists
|
|
534
|
-
const data = await promises_1.default.readFile(filePath, "utf-8");
|
|
535
|
-
const state = JSON.parse(data);
|
|
536
|
-
if (state && typeof state === "object" && state.node.options.identifier === nodeId) {
|
|
537
|
-
await promises_1.default.unlink(filePath); // Delete the file asynchronously
|
|
538
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Deleted player state file: ${filePath}`);
|
|
659
|
+
catch (error) {
|
|
660
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error loading player states: ${error}`);
|
|
539
661
|
}
|
|
540
662
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
663
|
+
break;
|
|
664
|
+
case Enums_1.StateStorageType.Redis:
|
|
665
|
+
{
|
|
666
|
+
try {
|
|
667
|
+
// Get all keys matching our pattern
|
|
668
|
+
const redisKeyPattern = `${this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
669
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
670
|
+
: this.options.stateStorage.redisConfig.prefix ?? "magmastream:"}playerstore:*`;
|
|
671
|
+
const keys = await this.redis.keys(redisKeyPattern);
|
|
672
|
+
for (const key of keys) {
|
|
673
|
+
try {
|
|
674
|
+
const data = await this.redis.get(key);
|
|
675
|
+
if (!data)
|
|
676
|
+
continue;
|
|
677
|
+
const state = JSON.parse(data);
|
|
678
|
+
if (!state || typeof state !== "object" || state.clusterId !== this.options.clusterId)
|
|
679
|
+
continue;
|
|
680
|
+
const guildId = key.split(":").pop();
|
|
681
|
+
if (!guildId)
|
|
682
|
+
continue;
|
|
683
|
+
if (state.node?.options?.identifier === nodeId) {
|
|
684
|
+
const lavaPlayer = info.find((player) => player.guildId === guildId);
|
|
685
|
+
if (!lavaPlayer) {
|
|
686
|
+
await this.destroy(guildId);
|
|
687
|
+
}
|
|
688
|
+
const playerOptions = {
|
|
689
|
+
guildId: state.options.guildId,
|
|
690
|
+
textChannelId: state.options.textChannelId,
|
|
691
|
+
voiceChannelId: state.options.voiceChannelId,
|
|
692
|
+
selfDeafen: state.options.selfDeafen,
|
|
693
|
+
volume: lavaPlayer?.volume || state.options.volume,
|
|
694
|
+
nodeIdentifier: nodeId,
|
|
695
|
+
};
|
|
696
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Recreating player: ${guildId} from Redis`);
|
|
697
|
+
const player = this.create(playerOptions);
|
|
698
|
+
await player.node.rest.updatePlayer({
|
|
699
|
+
guildId: state.options.guildId,
|
|
700
|
+
data: { voice: { token: state.voiceState.event.token, endpoint: state.voiceState.event.endpoint, sessionId: state.voiceState.sessionId } },
|
|
701
|
+
});
|
|
702
|
+
player.connect();
|
|
703
|
+
// Rest of the player state restoration code (tracks, filters, etc.)
|
|
704
|
+
const tracks = [];
|
|
705
|
+
const currentTrack = state.queue.current;
|
|
706
|
+
const queueTracks = state.queue.tracks;
|
|
707
|
+
if (state.isAutoplay) {
|
|
708
|
+
Object.setPrototypeOf(state.data.clientUser, { constructor: { name: "User" } });
|
|
709
|
+
player.setAutoplay(true, state.data.clientUser, state.autoplayTries);
|
|
710
|
+
}
|
|
711
|
+
if (lavaPlayer?.track) {
|
|
712
|
+
// If lavaPlayer has a track, push all queue tracks
|
|
713
|
+
tracks.push(...queueTracks);
|
|
714
|
+
// Set current track if matches lavaPlayer's track URI
|
|
715
|
+
if (currentTrack && currentTrack.uri === lavaPlayer.track.info.uri) {
|
|
716
|
+
await player.queue.setCurrent(Utils_1.TrackUtils.build(lavaPlayer.track, currentTrack.requester));
|
|
717
|
+
}
|
|
718
|
+
// Add tracks to queue
|
|
719
|
+
if (tracks.length > 0) {
|
|
720
|
+
await player.queue.clear();
|
|
721
|
+
await player.queue.add(tracks);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
// LavaPlayer missing track or lavaPlayer is falsy
|
|
726
|
+
if (currentTrack) {
|
|
727
|
+
if (queueTracks.length > 0) {
|
|
728
|
+
tracks.push(...queueTracks);
|
|
729
|
+
await player.queue.clear();
|
|
730
|
+
await player.queue.add(tracks);
|
|
731
|
+
}
|
|
732
|
+
await node.trackEnd(player, currentTrack, {
|
|
733
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
734
|
+
type: "TrackEndEvent",
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// No current track, check previous queue for last track
|
|
739
|
+
const previousQueue = await player.queue.getPrevious();
|
|
740
|
+
const lastTrack = previousQueue?.at(-1);
|
|
741
|
+
if (lastTrack) {
|
|
742
|
+
if (queueTracks.length === 0) {
|
|
743
|
+
// If no tracks in queue, end last track
|
|
744
|
+
await node.trackEnd(player, lastTrack, {
|
|
745
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
746
|
+
type: "TrackEndEvent",
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
// If there are queued tracks, add them
|
|
751
|
+
tracks.push(...queueTracks);
|
|
752
|
+
if (tracks.length > 0) {
|
|
753
|
+
await player.queue.clear();
|
|
754
|
+
await player.queue.add(tracks);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
if (queueTracks.length > 0) {
|
|
760
|
+
tracks.push(...queueTracks);
|
|
761
|
+
if (tracks.length > 0) {
|
|
762
|
+
await player.queue.clear();
|
|
763
|
+
await player.queue.add(tracks);
|
|
764
|
+
}
|
|
765
|
+
await node.trackEnd(player, lastTrack, {
|
|
766
|
+
reason: Enums_1.TrackEndReasonTypes.Finished,
|
|
767
|
+
type: "TrackEndEvent",
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (state.queue.previous.length > 0) {
|
|
774
|
+
await player.queue.addPrevious(state.queue.previous);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
await player.queue.clearPrevious();
|
|
778
|
+
}
|
|
779
|
+
if (state.paused) {
|
|
780
|
+
await player.pause(true);
|
|
781
|
+
}
|
|
782
|
+
else {
|
|
783
|
+
player.paused = false;
|
|
784
|
+
}
|
|
785
|
+
if (state.trackRepeat)
|
|
786
|
+
player.setTrackRepeat(true);
|
|
787
|
+
if (state.queueRepeat)
|
|
788
|
+
player.setQueueRepeat(true);
|
|
789
|
+
if (state.dynamicRepeat) {
|
|
790
|
+
player.setDynamicRepeat(state.dynamicRepeat, state.dynamicLoopInterval._idleTimeout);
|
|
791
|
+
}
|
|
792
|
+
if (state.data) {
|
|
793
|
+
for (const [name, value] of Object.entries(state.data)) {
|
|
794
|
+
player.set(name, value);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const filterActions = {
|
|
798
|
+
bassboost: () => player.filters.bassBoost(state.filters.bassBoostlevel),
|
|
799
|
+
distort: (enabled) => player.filters.distort(enabled),
|
|
800
|
+
setDistortion: () => player.filters.setDistortion(state.filters.distortion),
|
|
801
|
+
eightD: (enabled) => player.filters.eightD(enabled),
|
|
802
|
+
setKaraoke: () => player.filters.setKaraoke(state.filters.karaoke),
|
|
803
|
+
nightcore: (enabled) => player.filters.nightcore(enabled),
|
|
804
|
+
slowmo: (enabled) => player.filters.slowmo(enabled),
|
|
805
|
+
soft: (enabled) => player.filters.soft(enabled),
|
|
806
|
+
trebleBass: (enabled) => player.filters.trebleBass(enabled),
|
|
807
|
+
setTimescale: () => player.filters.setTimescale(state.filters.timescale),
|
|
808
|
+
tv: (enabled) => player.filters.tv(enabled),
|
|
809
|
+
vibrato: () => player.filters.setVibrato(state.filters.vibrato),
|
|
810
|
+
vaporwave: (enabled) => player.filters.vaporwave(enabled),
|
|
811
|
+
pop: (enabled) => player.filters.pop(enabled),
|
|
812
|
+
party: (enabled) => player.filters.party(enabled),
|
|
813
|
+
earrape: (enabled) => player.filters.earrape(enabled),
|
|
814
|
+
electronic: (enabled) => player.filters.electronic(enabled),
|
|
815
|
+
radio: (enabled) => player.filters.radio(enabled),
|
|
816
|
+
setRotation: () => player.filters.setRotation(state.filters.rotation),
|
|
817
|
+
tremolo: (enabled) => player.filters.tremolo(enabled),
|
|
818
|
+
china: (enabled) => player.filters.china(enabled),
|
|
819
|
+
chipmunk: (enabled) => player.filters.chipmunk(enabled),
|
|
820
|
+
darthvader: (enabled) => player.filters.darthvader(enabled),
|
|
821
|
+
daycore: (enabled) => player.filters.daycore(enabled),
|
|
822
|
+
doubletime: (enabled) => player.filters.doubletime(enabled),
|
|
823
|
+
demon: (enabled) => player.filters.demon(enabled),
|
|
824
|
+
};
|
|
825
|
+
// Iterate through filterStatus and apply the enabled filters
|
|
826
|
+
for (const [filter, isEnabled] of Object.entries(state.filters.filterStatus)) {
|
|
827
|
+
if (isEnabled && filterActions[filter]) {
|
|
828
|
+
filterActions[filter](true);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
// After processing, delete the Redis key
|
|
832
|
+
await this.redis.del(key);
|
|
833
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted player state from Redis: ${key}`);
|
|
834
|
+
this.emit(Enums_1.ManagerEventTypes.PlayerRestored, player, node);
|
|
835
|
+
await this.sleep(1000);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error processing Redis key ${key}: ${error}`);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error loading player states from Redis: ${error}`);
|
|
846
|
+
}
|
|
544
847
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Error loading player states: ${error}`);
|
|
848
|
+
break;
|
|
849
|
+
default:
|
|
850
|
+
break;
|
|
549
851
|
}
|
|
550
|
-
this.emit(ManagerEventTypes.Debug, "[MANAGER] Finished loading saved players.");
|
|
852
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, "[MANAGER] Finished loading saved players.");
|
|
853
|
+
this.emit(Enums_1.ManagerEventTypes.RestoreComplete, node);
|
|
551
854
|
}
|
|
552
855
|
/**
|
|
553
856
|
* Returns the node to use based on the configured `useNode` and `enablePriorityMode` options.
|
|
@@ -559,7 +862,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
559
862
|
get useableNode() {
|
|
560
863
|
return this.options.enablePriorityMode
|
|
561
864
|
? this.priorityNode
|
|
562
|
-
: this.options.useNode === UseNodeOptions.LeastLoad
|
|
865
|
+
: this.options.useNode === Enums_1.UseNodeOptions.LeastLoad
|
|
563
866
|
? this.leastLoadNode.first()
|
|
564
867
|
: this.leastPlayersNode.first();
|
|
565
868
|
}
|
|
@@ -571,25 +874,28 @@ class Manager extends events_1.EventEmitter {
|
|
|
571
874
|
* After saving and cleaning up, it exits the process.
|
|
572
875
|
*/
|
|
573
876
|
async handleShutdown() {
|
|
877
|
+
this.unloadPlugins();
|
|
574
878
|
console.warn("\x1b[31m%s\x1b[0m", "MAGMASTREAM WARNING: Shutting down! Please wait, saving active players...");
|
|
575
879
|
try {
|
|
880
|
+
await this.clearAllStoredPlayers();
|
|
576
881
|
const savePromises = Array.from(this.players.keys()).map(async (guildId) => {
|
|
577
882
|
try {
|
|
578
883
|
await this.savePlayerState(guildId);
|
|
579
884
|
}
|
|
580
885
|
catch (error) {
|
|
581
|
-
console.error(`Error saving player state for guild ${guildId}:`, error);
|
|
886
|
+
console.error(`[MANAGER] Error saving player state for guild ${guildId}:`, error);
|
|
582
887
|
}
|
|
583
888
|
});
|
|
889
|
+
if (this.options.stateStorage.deleteInactivePlayers)
|
|
890
|
+
await this.cleanupInactivePlayers();
|
|
584
891
|
await Promise.allSettled(savePromises);
|
|
585
|
-
await this.cleanupInactivePlayers();
|
|
586
892
|
setTimeout(() => {
|
|
587
893
|
console.warn("\x1b[32m%s\x1b[0m", "MAGMASTREAM INFO: Shutting down complete, exiting...");
|
|
588
894
|
process.exit(0);
|
|
589
895
|
}, 500);
|
|
590
896
|
}
|
|
591
897
|
catch (error) {
|
|
592
|
-
console.error(
|
|
898
|
+
console.error(`[MANAGER] Unexpected error during shutdown:`, error);
|
|
593
899
|
process.exit(1);
|
|
594
900
|
}
|
|
595
901
|
}
|
|
@@ -707,6 +1013,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
707
1013
|
guildId: player.guildId,
|
|
708
1014
|
data: { voice: { token, endpoint, sessionId } },
|
|
709
1015
|
});
|
|
1016
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice server for player ${player.guildId} with token ${token} and endpoint ${endpoint} and sessionId ${sessionId}`);
|
|
710
1017
|
return;
|
|
711
1018
|
}
|
|
712
1019
|
/**
|
|
@@ -717,121 +1024,222 @@ class Manager extends events_1.EventEmitter {
|
|
|
717
1024
|
* @emits {playerDisconnect} - Emits a player disconnect event if the channel ID is null.
|
|
718
1025
|
*/
|
|
719
1026
|
async handleVoiceStateUpdate(player, update) {
|
|
1027
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `Updated voice state for player ${player.guildId} with channel id ${update.channel_id} and session id ${update.session_id}`);
|
|
720
1028
|
if (update.channel_id) {
|
|
721
1029
|
if (player.voiceChannelId !== update.channel_id) {
|
|
722
|
-
this.emit(ManagerEventTypes.PlayerMove, player, player.voiceChannelId, update.channel_id);
|
|
1030
|
+
this.emit(Enums_1.ManagerEventTypes.PlayerMove, player, player.voiceChannelId, update.channel_id);
|
|
723
1031
|
}
|
|
724
1032
|
player.voiceState.sessionId = update.session_id;
|
|
725
1033
|
player.voiceChannelId = update.channel_id;
|
|
1034
|
+
player.options.voiceChannelId = update.channel_id;
|
|
726
1035
|
return;
|
|
727
1036
|
}
|
|
728
|
-
this.emit(ManagerEventTypes.PlayerDisconnect, player, player.voiceChannelId);
|
|
1037
|
+
this.emit(Enums_1.ManagerEventTypes.PlayerDisconnect, player, player.voiceChannelId);
|
|
729
1038
|
player.voiceChannelId = null;
|
|
730
1039
|
player.voiceState = Object.assign({});
|
|
731
|
-
await player.
|
|
1040
|
+
await player.pause(true);
|
|
732
1041
|
return;
|
|
733
1042
|
}
|
|
734
1043
|
/**
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
* @returns {string} The path to the player's JSON file
|
|
1044
|
+
* Cleans up inactive players by removing their state files from the file system.
|
|
1045
|
+
* This is done to prevent stale state files from accumulating on the file system.
|
|
738
1046
|
*/
|
|
739
|
-
async
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1047
|
+
async cleanupInactivePlayers() {
|
|
1048
|
+
switch (this.options.stateStorage.type) {
|
|
1049
|
+
case Enums_1.StateStorageType.JSON:
|
|
1050
|
+
{
|
|
1051
|
+
const playersBaseDir = Utils_1.PlayerUtils.getPlayersBaseDir();
|
|
1052
|
+
try {
|
|
1053
|
+
await promises_1.default.mkdir(playersBaseDir, { recursive: true });
|
|
1054
|
+
const activeGuildIds = new Set(this.players.keys());
|
|
1055
|
+
// Cleanup inactive guild directories inside playersBaseDir
|
|
1056
|
+
const guildDirs = await promises_1.default.readdir(playersBaseDir, { withFileTypes: true });
|
|
1057
|
+
for (const dirent of guildDirs) {
|
|
1058
|
+
if (!dirent.isDirectory())
|
|
1059
|
+
continue;
|
|
1060
|
+
const guildId = dirent.name;
|
|
1061
|
+
if (!activeGuildIds.has(guildId)) {
|
|
1062
|
+
const guildPath = Utils_1.PlayerUtils.getGuildDir(guildId);
|
|
1063
|
+
await promises_1.default.rm(guildPath, { recursive: true, force: true });
|
|
1064
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted inactive player data folder: ${guildId}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error cleaning up inactive JSON players: ${error}`);
|
|
1070
|
+
}
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
break;
|
|
1074
|
+
case Enums_1.StateStorageType.Redis:
|
|
1075
|
+
{
|
|
1076
|
+
const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
1077
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
1078
|
+
: this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
|
|
1079
|
+
const pattern = `${prefix}queue:*:current`;
|
|
1080
|
+
const stream = this.redis.scanStream({
|
|
1081
|
+
match: pattern,
|
|
1082
|
+
count: 100,
|
|
1083
|
+
});
|
|
1084
|
+
for await (const keys of stream) {
|
|
1085
|
+
for (const key of keys) {
|
|
1086
|
+
// Extract guildId from queue key
|
|
1087
|
+
const match = key.match(new RegExp(`^${prefix}queue:(.+):current$`));
|
|
1088
|
+
if (!match)
|
|
1089
|
+
continue;
|
|
1090
|
+
const guildId = match[1];
|
|
1091
|
+
// If player is not active in memory, clean up all keys
|
|
1092
|
+
if (!this.players.has(guildId)) {
|
|
1093
|
+
await this.redis.del(`${prefix}playerstore:${guildId}`, `${prefix}queue:${guildId}:current`, `${prefix}queue:${guildId}:tracks`, `${prefix}queue:${guildId}:previous`);
|
|
1094
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleaned inactive Redis player data: ${guildId}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
break;
|
|
1101
|
+
default:
|
|
1102
|
+
break;
|
|
744
1103
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Cleans up an inactive player by removing its state data.
|
|
1107
|
+
* This is done to prevent stale state data from accumulating.
|
|
1108
|
+
* @param guildId The guild ID of the player to clean up.
|
|
1109
|
+
*/
|
|
1110
|
+
async cleanupInactivePlayer(guildId) {
|
|
1111
|
+
switch (this.options.stateStorage.type) {
|
|
1112
|
+
case Enums_1.StateStorageType.JSON:
|
|
1113
|
+
{
|
|
1114
|
+
try {
|
|
1115
|
+
if (!this.players.has(guildId)) {
|
|
1116
|
+
const guildDir = Utils_1.PlayerUtils.getGuildDir(guildId);
|
|
1117
|
+
await promises_1.default.rm(guildDir, { recursive: true, force: true });
|
|
1118
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted inactive player data folder: ${guildId}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
if (error.code !== "ENOENT") {
|
|
1123
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error deleting player files for ${guildId}: ${error}`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
case Enums_1.StateStorageType.Redis:
|
|
1129
|
+
{
|
|
1130
|
+
const player = this.getPlayer(guildId);
|
|
1131
|
+
if (!player) {
|
|
1132
|
+
const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
1133
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
1134
|
+
: `${this.options.stateStorage.redisConfig.prefix ?? "magmastream"}:`;
|
|
1135
|
+
const keysToDelete = [
|
|
1136
|
+
`${prefix}playerstore:${guildId}`,
|
|
1137
|
+
`${prefix}queue:${guildId}:tracks`,
|
|
1138
|
+
`${prefix}queue:${guildId}:current`,
|
|
1139
|
+
`${prefix}queue:${guildId}:previous`,
|
|
1140
|
+
];
|
|
1141
|
+
await this.redis.del(...keysToDelete);
|
|
1142
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Deleted Redis player and queue data for: ${guildId}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
break;
|
|
1146
|
+
default:
|
|
1147
|
+
break;
|
|
748
1148
|
}
|
|
749
1149
|
}
|
|
750
1150
|
/**
|
|
751
|
-
*
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
* @param obj The object to serialize
|
|
760
|
-
* @returns The serialized object
|
|
761
|
-
*/
|
|
762
|
-
const serialize = (obj) => {
|
|
763
|
-
if (obj && typeof obj === "object") {
|
|
764
|
-
if (seen.has(obj))
|
|
765
|
-
return;
|
|
766
|
-
seen.add(obj);
|
|
1151
|
+
* Loads the enabled plugins.
|
|
1152
|
+
*/
|
|
1153
|
+
loadPlugins() {
|
|
1154
|
+
if (!Array.isArray(this.options.enabledPlugins))
|
|
1155
|
+
return;
|
|
1156
|
+
for (const [index, plugin] of this.options.enabledPlugins.entries()) {
|
|
1157
|
+
if (!(plugin instanceof __1.Plugin)) {
|
|
1158
|
+
throw new RangeError(`Plugin at index ${index} does not extend Plugin.`);
|
|
767
1159
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return null;
|
|
1160
|
+
try {
|
|
1161
|
+
plugin.load(this);
|
|
1162
|
+
this.loadedPlugins.add(plugin);
|
|
1163
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[PLUGIN] Loaded plugin: ${plugin.name}`);
|
|
773
1164
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
return null;
|
|
777
|
-
return {
|
|
778
|
-
distortion: value.distortion ?? null,
|
|
779
|
-
equalizer: value.equalizer ?? [],
|
|
780
|
-
karaoke: value.karaoke ?? null,
|
|
781
|
-
rotation: value.rotation ?? null,
|
|
782
|
-
timescale: value.timescale ?? null,
|
|
783
|
-
vibrato: value.vibrato ?? null,
|
|
784
|
-
reverb: value.reverb ?? null,
|
|
785
|
-
volume: value.volume ?? 1.0,
|
|
786
|
-
bassBoostlevel: value.bassBoostlevel ?? null,
|
|
787
|
-
filterStatus: value.filtersStatus ? { ...value.filtersStatus } : {},
|
|
788
|
-
};
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[PLUGIN] Failed to load plugin "${plugin.name}": ${err}`);
|
|
789
1167
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Unloads the enabled plugins.
|
|
1172
|
+
*/
|
|
1173
|
+
unloadPlugins() {
|
|
1174
|
+
for (const plugin of this.loadedPlugins) {
|
|
1175
|
+
try {
|
|
1176
|
+
plugin.unload(this);
|
|
1177
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[PLUGIN] Unloaded plugin: ${plugin.name}`);
|
|
796
1178
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
clientUser: value.Internal_BotUser ?? null,
|
|
800
|
-
};
|
|
1179
|
+
catch (err) {
|
|
1180
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[PLUGIN] Failed to unload plugin "${plugin.name}": ${err}`);
|
|
801
1181
|
}
|
|
802
|
-
|
|
803
|
-
|
|
1182
|
+
}
|
|
1183
|
+
this.loadedPlugins.clear();
|
|
804
1184
|
}
|
|
805
1185
|
/**
|
|
806
|
-
*
|
|
1186
|
+
* Clears all player states from the file system.
|
|
807
1187
|
* This is done to prevent stale state files from accumulating on the file system.
|
|
808
1188
|
*/
|
|
809
|
-
async
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
for (const file of playerFiles) {
|
|
823
|
-
// Get the guild ID from the file name
|
|
824
|
-
const guildId = path_1.default.basename(file, ".json");
|
|
825
|
-
// If the guild ID is not in the set of active guild IDs, delete the file
|
|
826
|
-
if (!activeGuildIds.has(guildId)) {
|
|
827
|
-
const filePath = path_1.default.join(playerStatesDir, file);
|
|
828
|
-
await promises_1.default.unlink(filePath); // Delete the file asynchronously
|
|
829
|
-
this.emit(ManagerEventTypes.Debug, `[MANAGER] Deleting inactive player: ${guildId}`);
|
|
1189
|
+
async clearAllStoredPlayers() {
|
|
1190
|
+
switch (this.options.stateStorage.type) {
|
|
1191
|
+
case Enums_1.StateStorageType.Memory:
|
|
1192
|
+
case Enums_1.StateStorageType.JSON: {
|
|
1193
|
+
const playersBaseDir = Utils_1.PlayerUtils.getPlayersBaseDir();
|
|
1194
|
+
try {
|
|
1195
|
+
await promises_1.default.access(playersBaseDir).catch(async () => {
|
|
1196
|
+
await promises_1.default.mkdir(playersBaseDir, { recursive: true });
|
|
1197
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Created directory: ${playersBaseDir}`);
|
|
1198
|
+
});
|
|
1199
|
+
const files = await promises_1.default.readdir(playersBaseDir);
|
|
1200
|
+
await Promise.all(files.map((file) => promises_1.default.unlink(path_1.default.join(playersBaseDir, file)).catch((err) => this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Failed to delete file ${file}: ${err}`))));
|
|
1201
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared all player state files in ${playersBaseDir}`);
|
|
830
1202
|
}
|
|
1203
|
+
catch (err) {
|
|
1204
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Error clearing player state files: ${err}`);
|
|
1205
|
+
}
|
|
1206
|
+
break;
|
|
831
1207
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1208
|
+
case Enums_1.StateStorageType.Redis: {
|
|
1209
|
+
const prefix = this.options.stateStorage.redisConfig.prefix?.endsWith(":")
|
|
1210
|
+
? this.options.stateStorage.redisConfig.prefix
|
|
1211
|
+
: this.options.stateStorage.redisConfig.prefix ?? "magmastream:";
|
|
1212
|
+
const patterns = [`${prefix}playerstore:*`, `${prefix}queue:*`];
|
|
1213
|
+
try {
|
|
1214
|
+
for (const pattern of patterns) {
|
|
1215
|
+
const stream = this.redis.scanStream({
|
|
1216
|
+
match: pattern,
|
|
1217
|
+
count: 100,
|
|
1218
|
+
});
|
|
1219
|
+
let totalDeleted = 0;
|
|
1220
|
+
stream.on("data", async (keys) => {
|
|
1221
|
+
if (keys.length) {
|
|
1222
|
+
const pipeline = this.redis.pipeline();
|
|
1223
|
+
keys.forEach((key) => pipeline.unlink(key));
|
|
1224
|
+
await pipeline.exec();
|
|
1225
|
+
totalDeleted += keys.length;
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
stream.on("end", () => {
|
|
1229
|
+
this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Cleared ${totalDeleted} Redis keys (pattern: ${pattern})`);
|
|
1230
|
+
});
|
|
1231
|
+
stream.on("error", (err) => {
|
|
1232
|
+
console.error(`[MANAGER] Error during Redis SCAN stream (${pattern}):`, err);
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
catch (err) {
|
|
1237
|
+
console.error("[MANAGER] Failed to clear Redis keys:", err);
|
|
1238
|
+
}
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
default:
|
|
1242
|
+
console.warn("[MANAGER] No valid stateStorage.type set, skipping state clearing.");
|
|
835
1243
|
}
|
|
836
1244
|
}
|
|
837
1245
|
/**
|
|
@@ -842,7 +1250,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
842
1250
|
*/
|
|
843
1251
|
get leastLoadNode() {
|
|
844
1252
|
return this.nodes
|
|
845
|
-
.filter((node) => node.connected)
|
|
1253
|
+
.filter((node) => node.connected && !node.options.isBackup)
|
|
846
1254
|
.sort((a, b) => {
|
|
847
1255
|
const aload = a.stats.cpu ? (a.stats.cpu.lavalinkLoad / a.stats.cpu.cores) * 100 : 0;
|
|
848
1256
|
const bload = b.stats.cpu ? (b.stats.cpu.lavalinkLoad / b.stats.cpu.cores) * 100 : 0;
|
|
@@ -857,9 +1265,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
857
1265
|
* @returns {Collection<string, Node>} A collection of nodes sorted by player count.
|
|
858
1266
|
*/
|
|
859
1267
|
get leastPlayersNode() {
|
|
860
|
-
return this.nodes
|
|
861
|
-
.filter((node) => node.connected) // Filter out nodes that are not connected
|
|
862
|
-
.sort((a, b) => a.stats.players - b.stats.players); // Sort by the number of players
|
|
1268
|
+
return this.nodes.filter((node) => node.connected && !node.options.isBackup).sort((a, b) => a.stats.players - b.stats.players);
|
|
863
1269
|
}
|
|
864
1270
|
/**
|
|
865
1271
|
* Returns a node based on priority.
|
|
@@ -892,112 +1298,17 @@ class Manager extends events_1.EventEmitter {
|
|
|
892
1298
|
}
|
|
893
1299
|
}
|
|
894
1300
|
// If no node has a cumulative weight greater than or equal to the random number, return the node with the lowest load
|
|
895
|
-
return this.options.useNode === UseNodeOptions.LeastLoad ? this.leastLoadNode.first() : this.leastPlayersNode.first();
|
|
1301
|
+
return this.options.useNode === Enums_1.UseNodeOptions.LeastLoad ? this.leastLoadNode.first() : this.leastPlayersNode.first();
|
|
1302
|
+
}
|
|
1303
|
+
send(packet) {
|
|
1304
|
+
if (!this._send) {
|
|
1305
|
+
console.warn("[Manager.send] _send is not defined! Packet will not be sent.");
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
return this._send(packet);
|
|
1309
|
+
}
|
|
1310
|
+
sendPacket(packet) {
|
|
1311
|
+
return this.send(packet);
|
|
896
1312
|
}
|
|
897
1313
|
}
|
|
898
1314
|
exports.Manager = Manager;
|
|
899
|
-
var StateStorageType;
|
|
900
|
-
(function (StateStorageType) {
|
|
901
|
-
StateStorageType["Collection"] = "collection";
|
|
902
|
-
StateStorageType["Redis"] = "redis";
|
|
903
|
-
})(StateStorageType || (exports.StateStorageType = StateStorageType = {}));
|
|
904
|
-
var TrackPartial;
|
|
905
|
-
(function (TrackPartial) {
|
|
906
|
-
/** The base64 encoded string of the track */
|
|
907
|
-
TrackPartial["Track"] = "track";
|
|
908
|
-
/** The title of the track */
|
|
909
|
-
TrackPartial["Title"] = "title";
|
|
910
|
-
/** The track identifier */
|
|
911
|
-
TrackPartial["Identifier"] = "identifier";
|
|
912
|
-
/** The author of the track */
|
|
913
|
-
TrackPartial["Author"] = "author";
|
|
914
|
-
/** The length of the track in milliseconds */
|
|
915
|
-
TrackPartial["Duration"] = "duration";
|
|
916
|
-
/** The ISRC of the track */
|
|
917
|
-
TrackPartial["Isrc"] = "isrc";
|
|
918
|
-
/** Whether the track is seekable */
|
|
919
|
-
TrackPartial["IsSeekable"] = "isSeekable";
|
|
920
|
-
/** Whether the track is a stream */
|
|
921
|
-
TrackPartial["IsStream"] = "isStream";
|
|
922
|
-
/** The URI of the track */
|
|
923
|
-
TrackPartial["Uri"] = "uri";
|
|
924
|
-
/** The artwork URL of the track */
|
|
925
|
-
TrackPartial["ArtworkUrl"] = "artworkUrl";
|
|
926
|
-
/** The source name of the track */
|
|
927
|
-
TrackPartial["SourceName"] = "sourceName";
|
|
928
|
-
/** The thumbnail of the track */
|
|
929
|
-
TrackPartial["ThumbNail"] = "thumbnail";
|
|
930
|
-
/** The requester of the track */
|
|
931
|
-
TrackPartial["Requester"] = "requester";
|
|
932
|
-
/** The plugin info of the track */
|
|
933
|
-
TrackPartial["PluginInfo"] = "pluginInfo";
|
|
934
|
-
/** The custom data of the track */
|
|
935
|
-
TrackPartial["CustomData"] = "customData";
|
|
936
|
-
})(TrackPartial || (exports.TrackPartial = TrackPartial = {}));
|
|
937
|
-
var UseNodeOptions;
|
|
938
|
-
(function (UseNodeOptions) {
|
|
939
|
-
UseNodeOptions["LeastLoad"] = "leastLoad";
|
|
940
|
-
UseNodeOptions["LeastPlayers"] = "leastPlayers";
|
|
941
|
-
})(UseNodeOptions || (exports.UseNodeOptions = UseNodeOptions = {}));
|
|
942
|
-
var SearchPlatform;
|
|
943
|
-
(function (SearchPlatform) {
|
|
944
|
-
SearchPlatform["AppleMusic"] = "amsearch";
|
|
945
|
-
SearchPlatform["Bandcamp"] = "bcsearch";
|
|
946
|
-
SearchPlatform["Deezer"] = "dzsearch";
|
|
947
|
-
SearchPlatform["Jiosaavn"] = "jssearch";
|
|
948
|
-
SearchPlatform["SoundCloud"] = "scsearch";
|
|
949
|
-
SearchPlatform["Spotify"] = "spsearch";
|
|
950
|
-
SearchPlatform["Tidal"] = "tdsearch";
|
|
951
|
-
SearchPlatform["VKMusic"] = "vksearch";
|
|
952
|
-
SearchPlatform["YouTube"] = "ytsearch";
|
|
953
|
-
SearchPlatform["YouTubeMusic"] = "ytmsearch";
|
|
954
|
-
})(SearchPlatform || (exports.SearchPlatform = SearchPlatform = {}));
|
|
955
|
-
var AutoPlayPlatform;
|
|
956
|
-
(function (AutoPlayPlatform) {
|
|
957
|
-
AutoPlayPlatform["Spotify"] = "spotify";
|
|
958
|
-
AutoPlayPlatform["Deezer"] = "deezer";
|
|
959
|
-
AutoPlayPlatform["SoundCloud"] = "soundcloud";
|
|
960
|
-
AutoPlayPlatform["Tidal"] = "tidal";
|
|
961
|
-
AutoPlayPlatform["VKMusic"] = "vkmusic";
|
|
962
|
-
AutoPlayPlatform["YouTube"] = "youtube";
|
|
963
|
-
})(AutoPlayPlatform || (exports.AutoPlayPlatform = AutoPlayPlatform = {}));
|
|
964
|
-
var PlayerStateEventTypes;
|
|
965
|
-
(function (PlayerStateEventTypes) {
|
|
966
|
-
PlayerStateEventTypes["AutoPlayChange"] = "playerAutoplay";
|
|
967
|
-
PlayerStateEventTypes["ConnectionChange"] = "playerConnection";
|
|
968
|
-
PlayerStateEventTypes["RepeatChange"] = "playerRepeat";
|
|
969
|
-
PlayerStateEventTypes["PauseChange"] = "playerPause";
|
|
970
|
-
PlayerStateEventTypes["QueueChange"] = "queueChange";
|
|
971
|
-
PlayerStateEventTypes["TrackChange"] = "trackChange";
|
|
972
|
-
PlayerStateEventTypes["VolumeChange"] = "volumeChange";
|
|
973
|
-
PlayerStateEventTypes["ChannelChange"] = "channelChange";
|
|
974
|
-
PlayerStateEventTypes["PlayerCreate"] = "playerCreate";
|
|
975
|
-
PlayerStateEventTypes["PlayerDestroy"] = "playerDestroy";
|
|
976
|
-
})(PlayerStateEventTypes || (exports.PlayerStateEventTypes = PlayerStateEventTypes = {}));
|
|
977
|
-
var ManagerEventTypes;
|
|
978
|
-
(function (ManagerEventTypes) {
|
|
979
|
-
ManagerEventTypes["Debug"] = "debug";
|
|
980
|
-
ManagerEventTypes["NodeCreate"] = "nodeCreate";
|
|
981
|
-
ManagerEventTypes["NodeDestroy"] = "nodeDestroy";
|
|
982
|
-
ManagerEventTypes["NodeConnect"] = "nodeConnect";
|
|
983
|
-
ManagerEventTypes["NodeReconnect"] = "nodeReconnect";
|
|
984
|
-
ManagerEventTypes["NodeDisconnect"] = "nodeDisconnect";
|
|
985
|
-
ManagerEventTypes["NodeError"] = "nodeError";
|
|
986
|
-
ManagerEventTypes["NodeRaw"] = "nodeRaw";
|
|
987
|
-
ManagerEventTypes["PlayerCreate"] = "playerCreate";
|
|
988
|
-
ManagerEventTypes["PlayerDestroy"] = "playerDestroy";
|
|
989
|
-
ManagerEventTypes["PlayerStateUpdate"] = "playerStateUpdate";
|
|
990
|
-
ManagerEventTypes["PlayerMove"] = "playerMove";
|
|
991
|
-
ManagerEventTypes["PlayerDisconnect"] = "playerDisconnect";
|
|
992
|
-
ManagerEventTypes["QueueEnd"] = "queueEnd";
|
|
993
|
-
ManagerEventTypes["SocketClosed"] = "socketClosed";
|
|
994
|
-
ManagerEventTypes["TrackStart"] = "trackStart";
|
|
995
|
-
ManagerEventTypes["TrackEnd"] = "trackEnd";
|
|
996
|
-
ManagerEventTypes["TrackStuck"] = "trackStuck";
|
|
997
|
-
ManagerEventTypes["TrackError"] = "trackError";
|
|
998
|
-
ManagerEventTypes["SegmentsLoaded"] = "segmentsLoaded";
|
|
999
|
-
ManagerEventTypes["SegmentSkipped"] = "segmentSkipped";
|
|
1000
|
-
ManagerEventTypes["ChapterStarted"] = "chapterStarted";
|
|
1001
|
-
ManagerEventTypes["ChaptersLoaded"] = "chaptersLoaded";
|
|
1002
|
-
})(ManagerEventTypes || (exports.ManagerEventTypes = ManagerEventTypes = {}));
|
|
1003
|
-
// PlayerStore WILL BE REMOVED IF YOU DONT FIND A USE FOR IT.
|