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