magmastream 2.9.3-dev.2 → 2.9.3-dev.21

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.
@@ -82,7 +82,7 @@ class Player {
82
82
  message: "Manager instance is required.",
83
83
  });
84
84
  }
85
- this.clusterId = this.manager.options.clusterId || 0;
85
+ this.clusterId = this.manager.options.clusterId ?? 0;
86
86
  // Check the player options for errors.
87
87
  (0, playerCheck_1.default)(options);
88
88
  this.options = {
@@ -260,21 +260,30 @@ class Player {
260
260
  */
261
261
  async destroy(disconnect = true) {
262
262
  this.state = Enums_1.StateTypes.Destroying;
263
+ if (this.dynamicLoopInterval) {
264
+ clearInterval(this.dynamicLoopInterval);
265
+ this.dynamicLoopInterval = null;
266
+ }
267
+ if (this.voiceReceiverReconnectTimeout) {
268
+ clearTimeout(this.voiceReceiverReconnectTimeout);
269
+ this.voiceReceiverReconnectTimeout = null;
270
+ }
271
+ if (this.voiceReceiverWsClient) {
272
+ this.voiceReceiverWsClient.removeAllListeners();
273
+ this.voiceReceiverWsClient.close();
274
+ this.voiceReceiverWsClient = null;
275
+ }
263
276
  if (disconnect) {
264
- await this.disconnect().catch((err) => {
265
- console.warn(`[Player#destroy] Failed to disconnect player ${this.guildId}:`, err);
266
- });
277
+ await this.disconnect().catch(() => { });
267
278
  }
268
- await this.node.rest.destroyPlayer(this.guildId).catch((err) => {
269
- console.warn(`[Player#destroy] REST failed to destroy player ${this.guildId}:`, err);
270
- });
271
- await this.queue.clear();
272
- await this.queue.clearPrevious();
273
- await this.queue.setCurrent(null);
279
+ await this.node.rest.destroyPlayer(this.guildId).catch(() => { });
280
+ await this.queue.destroy();
281
+ this.nowPlayingMessage = undefined;
274
282
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerDestroy, this);
275
283
  const deleted = this.manager.players.delete(this.guildId);
276
- if (this.manager.options.stateStorage.deleteInactivePlayers)
284
+ if (this.manager.options.stateStorage.deleteInactivePlayers) {
277
285
  await this.manager.cleanupInactivePlayer(this.guildId);
286
+ }
278
287
  return deleted;
279
288
  }
280
289
  /**
@@ -806,11 +815,8 @@ class Player {
806
815
  if (currentPlayingTrack) {
807
816
  await this.queue.add(currentPlayingTrack, 0);
808
817
  }
809
- await this.play(lastTrack);
810
- }
811
- else {
812
- await this.play(lastTrack);
813
818
  }
819
+ await this.play(lastTrack);
814
820
  this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this, {
815
821
  changeType: Enums_1.PlayerStateEventTypes.TrackChange,
816
822
  details: {
@@ -922,7 +928,8 @@ class Player {
922
928
  const sessionId = this.voiceState?.sessionId;
923
929
  const token = this.voiceState?.event?.token;
924
930
  const endpoint = this.voiceState?.event?.endpoint;
925
- if (!sessionId || !token || !endpoint) {
931
+ const channelId = this.voiceState?.channelId;
932
+ if (!sessionId || !token || !endpoint || !channelId) {
926
933
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Voice state is not properly initialized for player ${this.guildId}. The bot might not be connected to a voice channel.`);
927
934
  throw new MagmastreamError_1.MagmaStreamError({
928
935
  code: Enums_1.MagmaStreamErrorCode.PLAYER_STATE_INVALID,
@@ -936,7 +943,7 @@ class Player {
936
943
  this.manager.players.set(this.guildId, this);
937
944
  await this.node.rest.updatePlayer({
938
945
  guildId: this.guildId,
939
- data: { paused: this.paused, volume: this.volume, position: playerPosition, encodedTrack: currentTrack?.track, voice: { token, endpoint, sessionId } },
946
+ data: { paused: this.paused, volume: this.volume, position: playerPosition, encodedTrack: currentTrack?.track, voice: { token, endpoint, sessionId, channelId } },
940
947
  });
941
948
  await this.filters.updateFilters();
942
949
  }
@@ -953,113 +960,6 @@ class Player {
953
960
  console.error(error);
954
961
  }
955
962
  }
956
- /**
957
- * Transfers the player to a new server. If the player already exists on the new server
958
- * and force is false, this method will return the existing player. Otherwise, a new player
959
- * will be created and the current player will be destroyed.
960
- * @param {PlayerOptions} newOptions - The new options for the player.
961
- * @param {boolean} force - Whether to force the creation of a new player.
962
- * @returns {Promise<Player>} - The new player instance.
963
- */
964
- async switchGuild(newOptions, force = false) {
965
- if (!newOptions.guildId) {
966
- throw new MagmastreamError_1.MagmaStreamError({
967
- code: Enums_1.MagmaStreamErrorCode.PLAYER_INVALID_CONFIG,
968
- message: "guildId is required for switchGuild",
969
- });
970
- }
971
- if (!newOptions.voiceChannelId) {
972
- throw new MagmastreamError_1.MagmaStreamError({
973
- code: Enums_1.MagmaStreamErrorCode.PLAYER_INVALID_CONFIG,
974
- message: "voiceChannelId is required for switchGuild",
975
- });
976
- }
977
- if (!newOptions.textChannelId) {
978
- throw new MagmastreamError_1.MagmaStreamError({
979
- code: Enums_1.MagmaStreamErrorCode.PLAYER_INVALID_CONFIG,
980
- message: "textChannelId is required for switchGuild",
981
- });
982
- }
983
- // Check if a player already exists for the new guild
984
- let newPlayer = this.manager.getPlayer(newOptions.guildId);
985
- // If the player already exists and force is false, return the existing player
986
- if (newPlayer && !force)
987
- return newPlayer;
988
- const oldPlayerProperties = {
989
- paused: this.paused,
990
- selfMute: this.options.selfMute,
991
- selfDeafen: this.options.selfDeafen,
992
- volume: this.volume,
993
- position: this.position,
994
- queue: {
995
- current: await this.queue.getCurrent(),
996
- tracks: [...(await this.queue.getTracks())],
997
- previous: [...(await this.queue.getPrevious())],
998
- },
999
- trackRepeat: this.trackRepeat,
1000
- queueRepeat: this.queueRepeat,
1001
- dynamicRepeat: this.dynamicRepeat,
1002
- dynamicRepeatIntervalMs: this.dynamicRepeatIntervalMs,
1003
- ClientUser: this.get("Internal_AutoplayUser"),
1004
- filters: this.filters,
1005
- nowPlayingMessage: this.nowPlayingMessage,
1006
- isAutoplay: this.isAutoplay,
1007
- applyVolumeAsFilter: this.options.applyVolumeAsFilter,
1008
- };
1009
- // If force is true, destroy the existing player for the new guild
1010
- if (force && newPlayer) {
1011
- await newPlayer.destroy();
1012
- }
1013
- newOptions.nodeIdentifier = newOptions.nodeIdentifier ?? this.options.nodeIdentifier;
1014
- newOptions.selfDeafen = newOptions.selfDeafen ?? oldPlayerProperties.selfDeafen;
1015
- newOptions.selfMute = newOptions.selfMute ?? oldPlayerProperties.selfMute;
1016
- newOptions.volume = newOptions.volume ?? oldPlayerProperties.volume;
1017
- newOptions.applyVolumeAsFilter = newOptions.applyVolumeAsFilter ?? oldPlayerProperties.applyVolumeAsFilter;
1018
- // Deep clone the current player
1019
- const clonedPlayer = this.manager.create(newOptions);
1020
- // Connect the cloned player to the new voice channel
1021
- clonedPlayer.connect();
1022
- // Update the player's state on the Lavalink node
1023
- await clonedPlayer.node.rest.updatePlayer({
1024
- guildId: clonedPlayer.guildId,
1025
- data: {
1026
- paused: oldPlayerProperties.paused,
1027
- volume: oldPlayerProperties.volume,
1028
- position: oldPlayerProperties.position,
1029
- encodedTrack: oldPlayerProperties.queue.current?.track,
1030
- },
1031
- });
1032
- await clonedPlayer.queue.setCurrent(oldPlayerProperties.queue.current);
1033
- await clonedPlayer.queue.addPrevious(oldPlayerProperties.queue.previous);
1034
- await clonedPlayer.queue.add(oldPlayerProperties.queue.tracks);
1035
- clonedPlayer.filters = oldPlayerProperties.filters;
1036
- clonedPlayer.isAutoplay = oldPlayerProperties.isAutoplay;
1037
- clonedPlayer.nowPlayingMessage = oldPlayerProperties.nowPlayingMessage;
1038
- clonedPlayer.trackRepeat = oldPlayerProperties.trackRepeat;
1039
- clonedPlayer.queueRepeat = oldPlayerProperties.queueRepeat;
1040
- clonedPlayer.dynamicRepeat = oldPlayerProperties.dynamicRepeat;
1041
- clonedPlayer.dynamicRepeatIntervalMs = oldPlayerProperties.dynamicRepeatIntervalMs;
1042
- clonedPlayer.set("Internal_AutoplayUser", oldPlayerProperties.ClientUser);
1043
- clonedPlayer.paused = oldPlayerProperties.paused;
1044
- // Update filters for the cloned player
1045
- await clonedPlayer.filters.updateFilters();
1046
- // Debug information
1047
- const debugInfo = {
1048
- success: true,
1049
- message: `Transferred ${await clonedPlayer.queue.size()} tracks successfully to <#${newOptions.voiceChannelId}> bound to <#${newOptions.textChannelId}>.`,
1050
- player: {
1051
- guildId: clonedPlayer.guildId,
1052
- voiceChannelId: clonedPlayer.voiceChannelId,
1053
- textChannelId: clonedPlayer.textChannelId,
1054
- volume: clonedPlayer.volume,
1055
- playing: clonedPlayer.playing,
1056
- queueSize: clonedPlayer.queue.size,
1057
- },
1058
- };
1059
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Transferred player to a new server: ${Utils_1.JSONUtils.safe(debugInfo, 2)}.`);
1060
- // Return the cloned player
1061
- return clonedPlayer;
1062
- }
1063
963
  /**
1064
964
  * Retrieves the data associated with the player.
1065
965
  * @returns {Record<string, unknown>} - The data associated with the player.
@@ -1074,6 +974,13 @@ class Player {
1074
974
  getDynamicLoopIntervalPublic() {
1075
975
  return this.dynamicLoopInterval;
1076
976
  }
977
+ /**
978
+ * Retrieves the data associated with the player.
979
+ * @returns {Record<string, unknown>} - The data associated with the player.
980
+ */
981
+ getSerializableData() {
982
+ return { ...this.data };
983
+ }
1077
984
  /**
1078
985
  * Retrieves the current lyrics for the playing track.
1079
986
  * @param skipTrackSource - Indicates whether to skip the track source when fetching lyrics.
@@ -104,35 +104,55 @@ class Rest {
104
104
  },
105
105
  data: body,
106
106
  timeout: this.node.options.apiRequestTimeoutMs,
107
+ validateStatus: () => true,
107
108
  };
109
+ let response;
108
110
  try {
109
- const response = await (0, axios_1.default)(config);
110
- return response.data;
111
+ response = await (0, axios_1.default)(config);
111
112
  }
112
113
  catch (err) {
113
- const error = err;
114
- if (!error.response) {
115
- throw new MagmastreamError_1.MagmaStreamError({
116
- code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
117
- message: `No response from node ${this.node.options.identifier}: ${error.message}`,
118
- });
119
- }
120
- const data = error.response.data;
121
- if (data?.message === "Guild not found") {
122
- return [];
123
- }
124
- if (error.response.status === 401) {
125
- throw new MagmastreamError_1.MagmaStreamError({
126
- code: Enums_1.MagmaStreamErrorCode.REST_UNAUTHORIZED,
127
- message: `Unauthorized access to node ${this.node.options.identifier}`,
128
- });
129
- }
130
- const dataMessage = typeof data === "string" ? data : data?.message ? data.message : Utils_1.JSONUtils.safe(data, 2);
114
+ const message = err instanceof Error ? err.message : "Unknown error";
131
115
  throw new MagmastreamError_1.MagmaStreamError({
132
116
  code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
133
- message: `Request to node ${this.node.options.identifier} failed with status ${error.response.status}: ${dataMessage}`,
117
+ message: `No response from node ${this.node.options.identifier}: ${message}`,
134
118
  });
135
119
  }
120
+ const { status, data } = response;
121
+ if (status >= 200 && status < 300) {
122
+ return data;
123
+ }
124
+ // Lavalink sometimes returns "Guild not found" for inactive players
125
+ if (status === 404 && data?.message === "Guild not found") {
126
+ return [];
127
+ }
128
+ if (status === 401) {
129
+ throw new MagmastreamError_1.MagmaStreamError({
130
+ code: Enums_1.MagmaStreamErrorCode.REST_UNAUTHORIZED,
131
+ message: `Unauthorized access to node ${this.node.options.identifier}`,
132
+ });
133
+ }
134
+ if (status >= 400 && status < 500) {
135
+ const message = typeof data === "string"
136
+ ? data
137
+ : typeof data === "object" && data !== null && "message" in data && typeof data.message === "string"
138
+ ? data.message
139
+ : "Unknown client error";
140
+ return {
141
+ status,
142
+ error: true,
143
+ message,
144
+ data,
145
+ };
146
+ }
147
+ const safeMessage = typeof data === "string"
148
+ ? data
149
+ : typeof data === "object" && data !== null && "message" in data && typeof data.message === "string"
150
+ ? data.message
151
+ : Utils_1.JSONUtils.safe(data, 2);
152
+ throw new MagmastreamError_1.MagmaStreamError({
153
+ code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
154
+ message: `Request to node ${this.node.options.identifier} failed (${status}): ${safeMessage}`,
155
+ });
136
156
  }
137
157
  /**
138
158
  * Sends a GET request to the specified endpoint and returns the response data.
@@ -9,9 +9,11 @@ const Enums_1 = require("./Enums");
9
9
  const path_1 = tslib_1.__importDefault(require("path"));
10
10
  const safe_stable_stringify_1 = tslib_1.__importDefault(require("safe-stable-stringify"));
11
11
  const MagmastreamError_1 = require("./MagmastreamError");
12
+ // import { isPlainObject } from "lodash";
12
13
  // import playwright from "playwright";
13
14
  /** @hidden */
14
15
  const SIZES = ["0", "1", "2", "3", "default", "mqdefault", "hqdefault", "maxresdefault"];
16
+ const REQUIRED_TRACK_KEYS = ["track", "title", "uri"];
15
17
  class TrackUtils {
16
18
  static trackPartial = null;
17
19
  static manager;
@@ -39,19 +41,12 @@ class TrackUtils {
39
41
  const defaultProperties = [
40
42
  Enums_1.TrackPartial.Track,
41
43
  Enums_1.TrackPartial.Title,
42
- Enums_1.TrackPartial.Identifier,
43
44
  Enums_1.TrackPartial.Author,
44
45
  Enums_1.TrackPartial.Duration,
45
- Enums_1.TrackPartial.Isrc,
46
- Enums_1.TrackPartial.IsSeekable,
47
- Enums_1.TrackPartial.IsStream,
48
46
  Enums_1.TrackPartial.Uri,
49
- Enums_1.TrackPartial.ArtworkUrl,
50
47
  Enums_1.TrackPartial.SourceName,
51
- Enums_1.TrackPartial.ThumbNail,
48
+ Enums_1.TrackPartial.ArtworkUrl,
52
49
  Enums_1.TrackPartial.Requester,
53
- Enums_1.TrackPartial.PluginInfo,
54
- Enums_1.TrackPartial.CustomData,
55
50
  ];
56
51
  /** The array of property names that will be removed from the Track class */
57
52
  this.trackPartial = Array.from(new Set([...defaultProperties, ...partial]));
@@ -61,33 +56,39 @@ class TrackUtils {
61
56
  }
62
57
  /**
63
58
  * Checks if the provided argument is a valid Track.
64
- * If provided an array then every element will be checked.
65
- * @param trackOrTracks The Track or array of Tracks to check.
59
+ * @param value The value to check.
66
60
  * @returns {boolean} Whether the provided argument is a valid Track.
67
61
  */
68
- static validate(trackOrTracks) {
69
- if (typeof trackOrTracks !== "object" || trackOrTracks === null) {
62
+ static isTrack(track) {
63
+ if (typeof track !== "object" || track === null)
70
64
  return false;
71
- }
72
- const isValidTrack = (track) => {
73
- if (typeof track !== "object" || track === null) {
74
- return false;
75
- }
76
- const t = track;
77
- return (typeof t.track === "string" && typeof t.title === "string" && typeof t.identifier === "string" && typeof t.isrc === "string" && typeof t.uri === "string");
78
- };
79
- if (Array.isArray(trackOrTracks)) {
80
- return trackOrTracks.every(isValidTrack);
81
- }
82
- return isValidTrack(trackOrTracks);
65
+ const t = track;
66
+ return REQUIRED_TRACK_KEYS.every((key) => typeof t[key] === "string");
67
+ }
68
+ /**
69
+ * Checks if the provided argument is a valid Track array.
70
+ * @param value The value to check.
71
+ * @returns {boolean} Whether the provided argument is a valid Track array.
72
+ */
73
+ static isTrackArray(value) {
74
+ return Array.isArray(value) && value.every(this.isTrack);
75
+ }
76
+ /**
77
+ * Checks if the provided argument is a valid Track or Track array.
78
+ * @param value The value to check.
79
+ * @returns {boolean} Whether the provided argument is a valid Track or Track array.
80
+ */
81
+ static validate(value) {
82
+ return this.isTrack(value) || this.isTrackArray(value);
83
83
  }
84
84
  /**
85
85
  * Builds a Track from the raw data from Lavalink and a optional requester.
86
86
  * @param data The raw data from Lavalink to build the Track from.
87
87
  * @param requester The user who requested the track, if any.
88
+ * @param isAutoPlay Whether the track is autoplayed. Defaults to false.
88
89
  * @returns The built Track.
89
90
  */
90
- static build(data, requester) {
91
+ static build(data, requester, isAutoplay = false) {
91
92
  if (typeof data === "undefined") {
92
93
  throw new MagmastreamError_1.MagmaStreamError({
93
94
  code: Enums_1.MagmaStreamErrorCode.UTILS_TRACK_BUILD_FAILED,
@@ -138,6 +139,7 @@ class TrackUtils {
138
139
  requester: requester,
139
140
  pluginInfo: data.pluginInfo,
140
141
  customData: {},
142
+ isAutoplay: isAutoplay,
141
143
  };
142
144
  track.displayThumbnail = track.displayThumbnail.bind(track);
143
145
  if (this.trackPartial) {
@@ -542,8 +544,7 @@ class AutoPlayUtils {
542
544
  return typeof data === "object" && data !== null && "encoded" in data && "info" in data;
543
545
  }
544
546
  static isTrackDataArray(data) {
545
- return (Array.isArray(data) &&
546
- data.every((track) => typeof track === "object" && track !== null && "encoded" in track && "info" in track && typeof track.encoded === "string"));
547
+ return (Array.isArray(data) && data.every((track) => typeof track === "object" && track !== null && "encoded" in track && "info" in track && typeof track.encoded === "string"));
547
548
  }
548
549
  static buildTracksFromResponse(recommendedResult, requester) {
549
550
  if (!recommendedResult)
@@ -560,7 +561,7 @@ class AutoPlayUtils {
560
561
  context: { recommendedResult },
561
562
  });
562
563
  }
563
- return [TrackUtils.build(data, requester)];
564
+ return [TrackUtils.build(data, requester, true)];
564
565
  }
565
566
  case Enums_1.LoadTypes.Short:
566
567
  case Enums_1.LoadTypes.Search: {
@@ -572,7 +573,7 @@ class AutoPlayUtils {
572
573
  context: { recommendedResult },
573
574
  });
574
575
  }
575
- return data.map((d) => TrackUtils.build(d, requester));
576
+ return data.map((d) => TrackUtils.build(d, requester, true));
576
577
  }
577
578
  case Enums_1.LoadTypes.Album:
578
579
  case Enums_1.LoadTypes.Artist:
@@ -582,7 +583,7 @@ class AutoPlayUtils {
582
583
  case Enums_1.LoadTypes.Playlist: {
583
584
  const data = recommendedResult.data;
584
585
  if (this.isPlaylistRawData(data)) {
585
- return data.tracks.map((d) => TrackUtils.build(d, requester));
586
+ return data.tracks.map((d) => TrackUtils.build(d, requester, true));
586
587
  }
587
588
  throw new MagmastreamError_1.MagmaStreamError({
588
589
  code: Enums_1.MagmaStreamErrorCode.UTILS_AUTOPLAY_BUILD_FAILED,
@@ -622,64 +623,114 @@ class PlayerUtils {
622
623
  * @returns The serialized Player instance
623
624
  */
624
625
  static async serializePlayer(player) {
626
+ const isNonSerializable = (value) => {
627
+ if (typeof value === "function" || typeof value === "symbol")
628
+ return true;
629
+ if (typeof value === "object" && value !== null) {
630
+ const ctorName = value.constructor?.name ?? "";
631
+ return (value instanceof Map ||
632
+ value instanceof Set ||
633
+ value instanceof WeakMap ||
634
+ value instanceof WeakSet ||
635
+ ctorName === "Timeout" ||
636
+ ctorName === "Socket" ||
637
+ ctorName === "TLSSocket" ||
638
+ ctorName === "EventEmitter");
639
+ }
640
+ return false;
641
+ };
642
+ const safeSerialize = (obj) => {
643
+ if (!obj || typeof obj !== "object")
644
+ return obj;
645
+ const result = {};
646
+ for (const [k, v] of Object.entries(obj)) {
647
+ if (!isNonSerializable(v)) {
648
+ result[k] = v;
649
+ }
650
+ }
651
+ return result;
652
+ };
625
653
  try {
626
- const current = await player.queue.getCurrent();
627
- const tracks = await player.queue.getTracks();
628
- const previous = await player.queue.getPrevious();
629
- const serializeTrack = (track) => ({
630
- ...track,
631
- requester: track.requester ? { id: track.requester.id, username: track.requester.username } : null,
632
- });
633
- const safeNode = player.node
634
- ? JSON.parse(JSON.stringify(player.node, (key, value) => {
635
- if (key === "rest" || key === "players" || key === "shards" || key === "manager")
636
- return undefined;
637
- return value;
638
- }))
639
- : null;
640
- return JSON.parse(JSON.stringify(player, (key, value) => {
641
- if (key === "manager")
642
- return null;
643
- if (key === "node")
644
- return safeNode;
645
- if (key === "filters") {
654
+ const [current, tracks, previous] = await Promise.all([player.queue.getCurrent(), player.queue.getTracks(), player.queue.getPrevious()]);
655
+ const serializeTrack = (track) => {
656
+ try {
646
657
  return {
647
- distortion: value?.distortion ?? null,
648
- equalizer: value?.equalizer ?? [],
649
- karaoke: value?.karaoke ?? null,
650
- rotation: value?.rotation ?? null,
651
- timescale: value?.timescale ?? null,
652
- vibrato: value?.vibrato ?? null,
653
- reverb: value?.reverb ?? null,
654
- volume: value?.volume ?? 1.0,
655
- bassBoostlevel: value?.bassBoostlevel ?? null,
656
- filterStatus: value?.filtersStatus ? { ...value.filtersStatus } : {},
658
+ ...track,
659
+ requester: track.requester ? { id: track.requester.id, username: track.requester.username } : null,
657
660
  };
658
661
  }
659
- if (key === "queue") {
660
- return {
661
- current: current ? serializeTrack(current) : null,
662
- tracks: tracks.map(serializeTrack),
663
- previous: previous.map(serializeTrack),
664
- };
662
+ catch {
663
+ return null;
665
664
  }
666
- if (key === "data") {
667
- return {
668
- clientUser: value?.Internal_AutoplayUser ?? null,
669
- nowPlayingMessage: value?.nowPlayingMessage ?? null,
670
- };
665
+ };
666
+ const safeCurrent = current ? serializeTrack(current) : null;
667
+ const safeTracks = tracks.map(serializeTrack).filter((t) => t !== null);
668
+ const safePrevious = previous.map(serializeTrack).filter((t) => t !== null);
669
+ let safeNode = null;
670
+ if (player.node) {
671
+ try {
672
+ safeNode = JSON.parse(JSON.stringify(player.node, (key, value) => {
673
+ if (["rest", "players", "shards", "manager"].includes(key))
674
+ return undefined;
675
+ if (isNonSerializable(value))
676
+ return undefined;
677
+ return value;
678
+ }));
671
679
  }
672
- return value;
673
- }));
680
+ catch {
681
+ safeNode = null;
682
+ }
683
+ }
684
+ const snapshot = {
685
+ options: player.options,
686
+ voiceState: player.voiceState,
687
+ clusterId: player.clusterId,
688
+ guildId: player.guildId,
689
+ voiceChannelId: player.voiceChannelId ?? null,
690
+ textChannelId: player.textChannelId ?? null,
691
+ volume: player.volume,
692
+ paused: player.paused,
693
+ playing: player.playing,
694
+ position: player.position,
695
+ trackRepeat: player.trackRepeat,
696
+ queueRepeat: player.queueRepeat,
697
+ dynamicRepeat: player.dynamicRepeat,
698
+ node: safeNode,
699
+ queue: {
700
+ current: safeCurrent,
701
+ tracks: safeTracks,
702
+ previous: safePrevious,
703
+ },
704
+ filters: player.filters
705
+ ? {
706
+ distortion: player.filters.distortion ?? null,
707
+ equalizer: player.filters.equalizer ?? [],
708
+ karaoke: player.filters.karaoke ?? null,
709
+ rotation: player.filters.rotation ?? null,
710
+ timescale: player.filters.timescale ?? null,
711
+ vibrato: player.filters.vibrato ?? null,
712
+ reverb: player.filters.reverb ?? null,
713
+ volume: player.filters.volume ?? 1.0,
714
+ bassBoostlevel: player.filters.bassBoostlevel ?? null,
715
+ filterStatus: { ...player.filters.filtersStatus },
716
+ }
717
+ : null,
718
+ data: safeSerialize(player.getSerializableData()),
719
+ };
720
+ // Sanity check
721
+ JSON.stringify(snapshot);
722
+ return snapshot;
674
723
  }
675
724
  catch (err) {
676
- throw err instanceof MagmastreamError_1.MagmaStreamError
725
+ const error = err instanceof MagmastreamError_1.MagmaStreamError
677
726
  ? err
678
727
  : new MagmastreamError_1.MagmaStreamError({
679
- code: Enums_1.MagmaStreamErrorCode.MANAGER_SEARCH_FAILED,
680
- message: `An error occurred while searching: ${err instanceof Error ? err.message : String(err)}`,
728
+ code: Enums_1.MagmaStreamErrorCode.UTILS_PLAYER_SERIALIZE_FAILED,
729
+ message: `An error occurred while serializing player: ${err instanceof Error ? err.message : String(err)}`,
681
730
  cause: err instanceof Error ? err : undefined,
682
731
  });
732
+ console.error(error);
733
+ return null;
683
734
  }
684
735
  }
685
736
  /**
@@ -718,6 +769,16 @@ class PlayerUtils {
718
769
  static getPlayerPreviousPath(guildId) {
719
770
  return path_1.default.join(this.getGuildDir(guildId), "previous.json");
720
771
  }
772
+ /**
773
+ * Gets the Redis key for player storage.
774
+ */
775
+ static getRedisKey() {
776
+ const cfg = this.manager.options.stateStorage.redisConfig;
777
+ // Default prefix
778
+ let prefix = (cfg.prefix ?? "magmastream:").trim();
779
+ prefix = prefix.replace(/:+$/g, "") + ":";
780
+ return prefix;
781
+ }
721
782
  }
722
783
  exports.PlayerUtils = PlayerUtils;
723
784
  /** Gets or extends structures to extend the built in, or already extended, classes to add more functionality. */
@@ -4,7 +4,10 @@ exports.DiscordJSManager = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const Manager_1 = require("../structures/Manager");
6
6
  const discord_js_1 = require("discord.js");
7
- const [major, minor] = discord_js_1.version.split(".").map(Number);
7
+ const discord_js_2 = require("discord.js");
8
+ const MagmastreamError_1 = require("../structures/MagmastreamError");
9
+ const Enums_1 = require("../structures/Enums");
10
+ const [major, minor] = discord_js_2.version.split(".").map(Number);
8
11
  tslib_1.__exportStar(require("../index"), exports);
9
12
  /**
10
13
  * Discord.js wrapper for Magmastream.
@@ -14,6 +17,12 @@ class DiscordJSManager extends Manager_1.Manager {
14
17
  constructor(client, options) {
15
18
  super(options);
16
19
  this.client = client;
20
+ if (!this.client.options.intents.has(discord_js_1.GatewayIntentBits.GuildVoiceStates)) {
21
+ throw new MagmastreamError_1.MagmaStreamError({
22
+ code: Enums_1.MagmaStreamErrorCode.INTENT_MISSING,
23
+ message: "[Custom Wrapper] Your Discord.js client must have the GuildVoiceStates intent enabled.",
24
+ });
25
+ }
17
26
  const attachReadyHandler = () => {
18
27
  const handler = () => {
19
28
  if (!this.options.clientId)
@@ -21,11 +30,11 @@ class DiscordJSManager extends Manager_1.Manager {
21
30
  };
22
31
  // Only attach clientReady if Discord.js >= 14.22.0
23
32
  if (major > 14 || (major === 14 && minor >= 22)) {
24
- client.once("clientReady", handler);
33
+ this.client.once("clientReady", handler);
25
34
  }
26
35
  // Only attach ready if Discord.js < 14.22.0
27
36
  if (major < 14 || (major === 14 && minor < 22)) {
28
- client.once("ready", handler);
37
+ this.client.once("ready", handler);
29
38
  }
30
39
  };
31
40
  attachReadyHandler();
@@ -39,7 +48,7 @@ class DiscordJSManager extends Manager_1.Manager {
39
48
  guild.shard.send(packet);
40
49
  }
41
50
  async resolveUser(user) {
42
- const id = typeof user === "string" ? user : user.id;
51
+ const id = typeof user === "string" ? user : String(user.id);
43
52
  const cached = this.client.users.cache.get(id);
44
53
  if (cached)
45
54
  return cached;
@@ -51,5 +60,11 @@ class DiscordJSManager extends Manager_1.Manager {
51
60
  return { id, username: typeof user === "string" ? undefined : user.username };
52
61
  }
53
62
  }
63
+ resolveGuild(guildId) {
64
+ const cached = this.client.guilds.cache.get(guildId);
65
+ if (cached)
66
+ return cached;
67
+ return null;
68
+ }
54
69
  }
55
70
  exports.DiscordJSManager = DiscordJSManager;