magmastream 2.10.3-alpha.5 → 2.10.3-alpha.6

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.
@@ -43,6 +43,8 @@ export declare enum LoadTypes {
43
43
  /** Nodelink */
44
44
  Artist = "artist",
45
45
  /** Nodelink */
46
+ Episode = "episode",
47
+ /** Nodelink */
46
48
  Station = "station",
47
49
  /** Nodelink */
48
50
  Podcast = "podcast",
@@ -50,6 +50,8 @@ var LoadTypes;
50
50
  /** Nodelink */
51
51
  LoadTypes["Artist"] = "artist";
52
52
  /** Nodelink */
53
+ LoadTypes["Episode"] = "episode";
54
+ /** Nodelink */
53
55
  LoadTypes["Station"] = "station";
54
56
  /** Nodelink */
55
57
  LoadTypes["Podcast"] = "podcast";
@@ -4,6 +4,7 @@ import { Manager } from "./Manager";
4
4
  import { Player } from "./Player";
5
5
  import { ChannelMixOptions, DistortionOptions, KaraokeOptions, LowPassOptions, ReverbOptions, RotationOptions, TimescaleOptions, TremoloOptions, VibratoOptions } from "./Types";
6
6
  export declare class Filters {
7
+ private static readonly defaultPluginFilterPlugins;
7
8
  distortion: DistortionOptions | null;
8
9
  equalizer: Band[];
9
10
  karaoke: KaraokeOptions | null;
@@ -46,6 +47,7 @@ export declare class Filters {
46
47
  */
47
48
  private applyFilter;
48
49
  private emitPlayersTasteUpdate;
50
+ private assertPluginFiltersSupported;
49
51
  /**
50
52
  * Sets the status of a specific filter.
51
53
  *
@@ -141,9 +143,10 @@ export declare class Filters {
141
143
  * Sets plugin-provided filter options on the audio.
142
144
  *
143
145
  * @param {Record<string, unknown>} [pluginFilters] - Plugin filter settings keyed by plugin filter name.
146
+ * @param {string | string[]} [requiredPluginNames] - The plugin name(s) that can handle the provided filters. Defaults to LavaDSPX.
144
147
  * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
145
148
  */
146
- setPluginFilters(pluginFilters?: Record<string, unknown>): Promise<this>;
149
+ setPluginFilters(pluginFilters?: Record<string, unknown>, requiredPluginNames?: string | string[]): Promise<this>;
147
150
  /**
148
151
  * Sets the own rotation options effect to the audio.
149
152
  *
@@ -5,6 +5,7 @@ const filtersEqualizers_1 = require("../utils/filtersEqualizers");
5
5
  const Enums_1 = require("./Enums");
6
6
  const MagmastreamError_1 = require("./MagmastreamError");
7
7
  class Filters {
8
+ static defaultPluginFilterPlugins = ["lavadspx-plugin", "LavaDSPX-Plugin"];
8
9
  distortion;
9
10
  equalizer;
10
11
  karaoke;
@@ -113,6 +114,18 @@ class Filters {
113
114
  details: { action: "change" },
114
115
  });
115
116
  }
117
+ assertPluginFiltersSupported(pluginNames) {
118
+ const requiredPlugins = Array.isArray(pluginNames) ? pluginNames : [pluginNames];
119
+ const availablePlugins = this.player.node.info?.plugins ?? [];
120
+ const hasRequiredPlugin = requiredPlugins.some((name) => availablePlugins.some((plugin) => plugin.name.toLowerCase() === name.toLowerCase()));
121
+ if (!hasRequiredPlugin) {
122
+ throw new MagmastreamError_1.MagmaStreamError({
123
+ code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
124
+ message: `One of the following plugins must be present in the lavalink node: ${requiredPlugins.map((name) => `"${name}"`).join(" or ")}.`,
125
+ context: { identifier: this.player.node.options.identifier },
126
+ });
127
+ }
128
+ }
116
129
  /**
117
130
  * Sets the status of a specific filter.
118
131
  *
@@ -274,11 +287,15 @@ class Filters {
274
287
  * Sets plugin-provided filter options on the audio.
275
288
  *
276
289
  * @param {Record<string, unknown>} [pluginFilters] - Plugin filter settings keyed by plugin filter name.
290
+ * @param {string | string[]} [requiredPluginNames] - The plugin name(s) that can handle the provided filters. Defaults to LavaDSPX.
277
291
  * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
278
292
  */
279
- async setPluginFilters(pluginFilters) {
293
+ async setPluginFilters(pluginFilters, requiredPluginNames = Filters.defaultPluginFilterPlugins) {
280
294
  const oldPlayer = { ...this };
281
295
  const nextPluginFilters = pluginFilters ?? {};
296
+ if (Object.keys(nextPluginFilters).length > 0) {
297
+ this.assertPluginFiltersSupported(requiredPluginNames);
298
+ }
282
299
  await this.applyFilter({ property: "pluginFilters", value: nextPluginFilters });
283
300
  this.setFilterStatus(Enums_1.AvailableFilters.PluginFilters, Object.keys(nextPluginFilters).length > 0);
284
301
  this.emitPlayersTasteUpdate(oldPlayer);
@@ -231,6 +231,7 @@ class Manager extends events_1.EventEmitter {
231
231
  result = { loadType: lavalinkResponse.loadType, tracks };
232
232
  break;
233
233
  }
234
+ case Enums_1.LoadTypes.Episode:
234
235
  case Enums_1.LoadTypes.Short:
235
236
  case Enums_1.LoadTypes.Track: {
236
237
  const track = Utils_1.TrackUtils.build(lavalinkResponse.data, requester);
@@ -158,6 +158,7 @@ export declare class Node {
158
158
  * @private
159
159
  */
160
160
  protected handleEvent(payload: PlayerEvent & PlayerEvents): Promise<void>;
161
+ private handleNodeLinkEvent;
161
162
  /**
162
163
  * Emitted when a new track starts playing.
163
164
  * @param {Player} player The player that started playing the track.
@@ -371,7 +371,10 @@ class Node {
371
371
  * @param request - The incoming message.
372
372
  */
373
373
  upgrade(request) {
374
- this.isNodeLink = this.options.isNodeLink ?? Boolean(request.headers.isnodelink) ?? false;
374
+ const nodeLinkHeader = request.headers.iamnodelink ?? request.headers.isnodelink;
375
+ const isNodeLinkHeader = Array.isArray(nodeLinkHeader) ? nodeLinkHeader.some((value) => value === "true") : nodeLinkHeader === "true";
376
+ this.isNodeLink = Boolean(this.options.isNodeLink) || isNodeLinkHeader;
377
+ this.rest.isNodeLink = this.isNodeLink;
375
378
  }
376
379
  /**
377
380
  * Handles the "open" event emitted by the WebSocket connection.
@@ -507,6 +510,8 @@ class Node {
507
510
  this.sessionId = payload.sessionId;
508
511
  await this.updateSessionId();
509
512
  this.info = await this.fetchInfo();
513
+ this.isNodeLink = this.isNodeLink || Boolean(this.info.isNodelink);
514
+ this.rest.isNodeLink = this.isNodeLink;
510
515
  if (payload.resumed || !hadPreviousSession) {
511
516
  await this.manager.loadPlayerStates(this.options.identifier);
512
517
  }
@@ -587,11 +592,71 @@ class Node {
587
592
  this.lyricsLine(player, track, payload);
588
593
  break;
589
594
  default:
595
+ if (this.isNodeLink && this.handleNodeLinkEvent(player, payload))
596
+ return;
590
597
  error = new Error(`Node#event unknown event '${type}'.`);
591
598
  this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
592
599
  break;
593
600
  }
594
601
  }
602
+ handleNodeLinkEvent(player, payload) {
603
+ switch (payload.type) {
604
+ case "VolumeChangedEvent": {
605
+ if (typeof payload.volume === "number") {
606
+ const oldVolume = player.volume;
607
+ const oldPlayer = { ...player };
608
+ player.volume = payload.volume;
609
+ this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
610
+ changeType: Enums_1.PlayerStateEventTypes.VolumeChange,
611
+ details: {
612
+ type: "volume",
613
+ action: "adjust",
614
+ previousVolume: oldVolume,
615
+ currentVolume: player.volume,
616
+ },
617
+ });
618
+ }
619
+ return true;
620
+ }
621
+ case "PauseEvent": {
622
+ if (typeof payload.paused === "boolean") {
623
+ const oldPaused = player.paused;
624
+ const oldPlayer = { ...player };
625
+ player.paused = payload.paused;
626
+ player.playing = !payload.paused;
627
+ this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
628
+ changeType: Enums_1.PlayerStateEventTypes.PauseChange,
629
+ details: {
630
+ type: "pause",
631
+ action: payload.paused ? "pause" : "resume",
632
+ previousPause: oldPaused,
633
+ currentPause: player.paused,
634
+ },
635
+ });
636
+ }
637
+ return true;
638
+ }
639
+ case "SeekEvent":
640
+ if (typeof payload.position === "number")
641
+ player.position = payload.position;
642
+ return true;
643
+ case "PlayerCreatedEvent":
644
+ case "PlayerDestroyedEvent":
645
+ case "PlayerConnectedEvent":
646
+ case "PlayerReconnectingEvent":
647
+ case "ConnectionStatusEvent":
648
+ case "FiltersChangedEvent":
649
+ case "MixStartedEvent":
650
+ case "MixEndedEvent":
651
+ case "EternalBoxInfoEvent":
652
+ case "EternalBoxJumpEvent":
653
+ case "StreamMetadataEvent":
654
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] NodeLink player event for guild ${player.guildId}: ${Utils_1.JSONUtils.safe(payload, 2)}`);
655
+ return true;
656
+ default:
657
+ return false;
658
+ }
659
+ }
595
660
  /**
596
661
  * Emitted when a new track starts playing.
597
662
  * @param {Player} player The player that started playing the track.
@@ -849,7 +914,7 @@ class Node {
849
914
  });
850
915
  }
851
916
  if (this.isNodeLink) {
852
- return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&language=${language}` : ""}`));
917
+ return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&lang=${encodeURIComponent(language)}` : ""}`));
853
918
  }
854
919
  const requiredPlugins = ["lavalyrics-plugin"];
855
920
  for (const plugin of requiredPlugins) {
@@ -886,13 +951,8 @@ class Node {
886
951
  context: { identifier: this.options.identifier },
887
952
  });
888
953
  }
889
- if (this.isNodeLink) {
890
- throw new MagmastreamError_1.MagmaStreamError({
891
- code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
892
- message: `The node is a NodeLink, cannot subscribe to lyrics.`,
893
- context: { identifier: this.options.identifier },
894
- });
895
- }
954
+ if (this.isNodeLink)
955
+ return await this.rest.post(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource}`, {});
896
956
  const requiredPlugins = ["lavalyrics-plugin"];
897
957
  for (const plugin of requiredPlugins) {
898
958
  if (!this.info.plugins.some((p) => p.name === plugin)) {
@@ -938,13 +998,8 @@ class Node {
938
998
  context: { identifier: this.options.identifier },
939
999
  });
940
1000
  }
941
- if (this.isNodeLink) {
942
- throw new MagmastreamError_1.MagmaStreamError({
943
- code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
944
- message: `The node is a NodeLink, cannot unsubscribe from lyrics.`,
945
- context: { identifier: this.options.identifier },
946
- });
947
- }
1001
+ if (this.isNodeLink)
1002
+ return await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe`);
948
1003
  if (!this.info.plugins.some((plugin) => plugin.name === "java-lyrics-plugin")) {
949
1004
  throw new MagmastreamError_1.MagmaStreamError({
950
1005
  code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
@@ -1088,6 +1143,22 @@ class Node {
1088
1143
  * @throws {RangeError} If the sponsorblock-plugin is not available in the Lavalink node.
1089
1144
  */
1090
1145
  async getSponsorBlock(player) {
1146
+ if (this.isNodeLink) {
1147
+ try {
1148
+ const state = (await this.rest.get(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`));
1149
+ return state.categories.map((segment) => segment);
1150
+ }
1151
+ catch (err) {
1152
+ throw err instanceof MagmastreamError_1.MagmaStreamError
1153
+ ? err
1154
+ : new MagmastreamError_1.MagmaStreamError({
1155
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1156
+ message: "Failed to fetch NodeLink SponsorBlock state.",
1157
+ cause: err instanceof Error ? err : undefined,
1158
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1159
+ });
1160
+ }
1161
+ }
1091
1162
  if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1092
1163
  throw new MagmastreamError_1.MagmaStreamError({
1093
1164
  code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
@@ -1124,13 +1195,6 @@ class Node {
1124
1195
  * ```
1125
1196
  */
1126
1197
  async setSponsorBlock(player, segments = [Enums_1.SponsorBlockSegment.Sponsor, Enums_1.SponsorBlockSegment.SelfPromo]) {
1127
- if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1128
- throw new MagmastreamError_1.MagmaStreamError({
1129
- code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
1130
- message: `The plugin "sponsorblock-plugin" must be present in the lavalink node to set SponsorBlock segments.`,
1131
- context: { identifier: this.options.identifier, guildId: player.guildId },
1132
- });
1133
- }
1134
1198
  if (!segments.length) {
1135
1199
  throw new MagmastreamError_1.MagmaStreamError({
1136
1200
  code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
@@ -1145,8 +1209,35 @@ class Node {
1145
1209
  context: { identifier: this.options.identifier, guildId: player.guildId, invalidSegments: segments },
1146
1210
  });
1147
1211
  }
1212
+ const categories = segments.map((segment) => segment.toLowerCase());
1213
+ if (this.isNodeLink) {
1214
+ try {
1215
+ await this.rest.patch(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`, {
1216
+ enabled: true,
1217
+ categories,
1218
+ });
1219
+ return;
1220
+ }
1221
+ catch (err) {
1222
+ throw err instanceof MagmastreamError_1.MagmaStreamError
1223
+ ? err
1224
+ : new MagmastreamError_1.MagmaStreamError({
1225
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1226
+ message: "Failed to set NodeLink SponsorBlock categories.",
1227
+ cause: err instanceof Error ? err : undefined,
1228
+ context: { identifier: this.options.identifier, guildId: player.guildId, segments },
1229
+ });
1230
+ }
1231
+ }
1232
+ if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1233
+ throw new MagmastreamError_1.MagmaStreamError({
1234
+ code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
1235
+ message: `The plugin "sponsorblock-plugin" must be present in the lavalink node to set SponsorBlock segments.`,
1236
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1237
+ });
1238
+ }
1148
1239
  try {
1149
- await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((segment) => segment.toLowerCase()));
1240
+ await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, categories);
1150
1241
  }
1151
1242
  catch (err) {
1152
1243
  throw err instanceof MagmastreamError_1.MagmaStreamError
@@ -1166,6 +1257,22 @@ class Node {
1166
1257
  * @throws {RangeError} If the sponsorblock-plugin is not available in the Lavalink node.
1167
1258
  */
1168
1259
  async deleteSponsorBlock(player) {
1260
+ if (this.isNodeLink) {
1261
+ try {
1262
+ await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`);
1263
+ return;
1264
+ }
1265
+ catch (err) {
1266
+ throw err instanceof MagmastreamError_1.MagmaStreamError
1267
+ ? err
1268
+ : new MagmastreamError_1.MagmaStreamError({
1269
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1270
+ message: "Failed to delete NodeLink SponsorBlock state.",
1271
+ cause: err instanceof Error ? err : undefined,
1272
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1273
+ });
1274
+ }
1275
+ }
1169
1276
  if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1170
1277
  throw new MagmastreamError_1.MagmaStreamError({
1171
1278
  code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
@@ -58,6 +58,7 @@ export declare class Player {
58
58
  protected voiceReceiverReconnectTimeout?: NodeJS.Timeout;
59
59
  protected voiceReceiverAttempt: number;
60
60
  protected voiceReceiverReconnectTries: number;
61
+ private readonly voiceReceiverStreams;
61
62
  /**
62
63
  * Creates a new player, returns one if it already exists.
63
64
  * @param options The player options.
@@ -329,10 +330,13 @@ export declare class Player {
329
330
  private openVoiceReceiver;
330
331
  /**
331
332
  * Handles a voice receiver message.
332
- * @param {string} payload - The payload to handle.
333
+ * @param {RawData} payload - The payload to handle.
333
334
  * @returns {Promise<void>} - A promise that resolves when the voice receiver message is handled.
334
335
  */
335
336
  private onVoiceReceiverMessage;
337
+ private toVoiceReceiverBuffer;
338
+ private parseNodeLinkVoiceFrame;
339
+ private handleNodeLinkVoiceFrame;
336
340
  /**
337
341
  * Handles a voice receiver error.
338
342
  * @param {Error} error - The error to handle.
@@ -61,11 +61,12 @@ class Player {
61
61
  dynamicRepeatIntervalMs = null;
62
62
  static _manager;
63
63
  /** Should only be used when the node is a NodeLink */
64
- voiceReceiverWsClient;
65
- isConnectToVoiceReceiver;
64
+ voiceReceiverWsClient = null;
65
+ isConnectToVoiceReceiver = false;
66
66
  voiceReceiverReconnectTimeout;
67
- voiceReceiverAttempt;
68
- voiceReceiverReconnectTries;
67
+ voiceReceiverAttempt = 1;
68
+ voiceReceiverReconnectTries = 30;
69
+ voiceReceiverStreams = new Map();
69
70
  /**
70
71
  * Creates a new player, returns one if it already exists.
71
72
  * @param options The player options.
@@ -270,6 +271,7 @@ class Player {
270
271
  this.voiceReceiverWsClient.close();
271
272
  this.voiceReceiverWsClient = null;
272
273
  }
274
+ this.voiceReceiverStreams.clear();
273
275
  if (disconnect) {
274
276
  await this.disconnect().catch(() => { });
275
277
  }
@@ -1040,10 +1042,11 @@ class Player {
1040
1042
  "Client-Name": this.manager.options.clientName,
1041
1043
  };
1042
1044
  const { host, useSSL, port } = this.node.options;
1043
- this.voiceReceiverWsClient = new ws_1.WebSocket(`${useSSL ? "wss" : "ws"}://${host}:${port}/connection/data`, { headers });
1045
+ this.voiceReceiverReconnectTries = this.node.options.maxRetryAttempts ?? this.voiceReceiverReconnectTries;
1046
+ this.voiceReceiverWsClient = new ws_1.WebSocket(`${useSSL ? "wss" : "ws"}://${host}:${port}/v4/websocket/voice/${this.guildId}`, { headers });
1044
1047
  this.voiceReceiverWsClient.on("open", () => this.openVoiceReceiver());
1045
1048
  this.voiceReceiverWsClient.on("error", (err) => this.onVoiceReceiverError(err));
1046
- this.voiceReceiverWsClient.on("message", (rawMessage) => this.onVoiceReceiverMessage(rawMessage.toString()));
1049
+ this.voiceReceiverWsClient.on("message", (rawMessage) => this.onVoiceReceiverMessage(rawMessage));
1047
1050
  this.voiceReceiverWsClient.on("close", (code, reason) => this.closeVoiceReceiver(code, reason.toString()));
1048
1051
  }
1049
1052
  /**
@@ -1065,6 +1068,7 @@ class Player {
1065
1068
  this.voiceReceiverWsClient = null;
1066
1069
  }
1067
1070
  this.isConnectToVoiceReceiver = false;
1071
+ this.voiceReceiverStreams.clear();
1068
1072
  }
1069
1073
  /**
1070
1074
  * Closes the voice receiver for the player.
@@ -1108,6 +1112,8 @@ class Player {
1108
1112
  this.voiceReceiverWsClient?.close(1000, "destroy");
1109
1113
  this.voiceReceiverWsClient?.removeAllListeners();
1110
1114
  this.voiceReceiverWsClient = null;
1115
+ this.voiceReceiverStreams.clear();
1116
+ this.isConnectToVoiceReceiver = false;
1111
1117
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Disconnected from voice receiver for player ${this.guildId}`);
1112
1118
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverDisconnect, this);
1113
1119
  }
@@ -1125,14 +1131,28 @@ class Player {
1125
1131
  }
1126
1132
  /**
1127
1133
  * Handles a voice receiver message.
1128
- * @param {string} payload - The payload to handle.
1134
+ * @param {RawData} payload - The payload to handle.
1129
1135
  * @returns {Promise<void>} - A promise that resolves when the voice receiver message is handled.
1130
1136
  */
1131
1137
  async onVoiceReceiverMessage(payload) {
1132
- const packet = JSON.parse(payload);
1138
+ const buffer = this.toVoiceReceiverBuffer(payload);
1139
+ const parsedFrame = this.parseNodeLinkVoiceFrame(buffer);
1140
+ if (parsedFrame) {
1141
+ this.handleNodeLinkVoiceFrame(parsedFrame);
1142
+ return;
1143
+ }
1144
+ const textPayload = buffer.toString();
1145
+ let packet;
1146
+ try {
1147
+ packet = JSON.parse(textPayload);
1148
+ }
1149
+ catch {
1150
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown binary payload for player ${this.guildId}`);
1151
+ return;
1152
+ }
1133
1153
  if (!packet?.op)
1134
1154
  return;
1135
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1155
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1136
1156
  switch (packet.type) {
1137
1157
  case "startSpeakingEvent": {
1138
1158
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, packet.data);
@@ -1141,17 +1161,86 @@ class Player {
1141
1161
  case "endSpeakingEvent": {
1142
1162
  const data = {
1143
1163
  ...packet.data,
1144
- data: Buffer.from(packet.data.data, "base64"),
1164
+ data: Buffer.isBuffer(packet.data.data) ? packet.data.data : Buffer.from(packet.data.data, "base64"),
1145
1165
  };
1146
1166
  this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverEndSpeaking, this, data);
1147
1167
  break;
1148
1168
  }
1149
1169
  default: {
1150
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1170
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
1151
1171
  break;
1152
1172
  }
1153
1173
  }
1154
1174
  }
1175
+ toVoiceReceiverBuffer(payload) {
1176
+ if (Buffer.isBuffer(payload))
1177
+ return payload;
1178
+ if (payload instanceof ArrayBuffer)
1179
+ return Buffer.from(payload);
1180
+ return Buffer.concat(payload);
1181
+ }
1182
+ parseNodeLinkVoiceFrame(payload) {
1183
+ if (payload.length < 12)
1184
+ return null;
1185
+ let offset = 0;
1186
+ const op = payload.readUInt8(offset++);
1187
+ if (op < 1 || op > 3)
1188
+ return null;
1189
+ const format = payload.readUInt8(offset++);
1190
+ const guildLength = payload.readUInt8(offset++);
1191
+ if (offset + guildLength > payload.length)
1192
+ return null;
1193
+ const guildId = payload.toString("utf8", offset, offset + guildLength);
1194
+ offset += guildLength;
1195
+ if (offset >= payload.length)
1196
+ return null;
1197
+ const userLength = payload.readUInt8(offset++);
1198
+ if (offset + userLength + 8 > payload.length)
1199
+ return null;
1200
+ const userId = payload.toString("utf8", offset, offset + userLength);
1201
+ offset += userLength;
1202
+ const ssrc = payload.readUInt32BE(offset);
1203
+ offset += 4;
1204
+ const timestamp = payload.readUInt32BE(offset);
1205
+ offset += 4;
1206
+ const type = format === 2 ? "pcm" : format === 1 ? "ogg" : "opus";
1207
+ return { op, type, guildId, userId, ssrc, timestamp, data: payload.subarray(offset) };
1208
+ }
1209
+ handleNodeLinkVoiceFrame(frame) {
1210
+ const key = `${frame.guildId}:${frame.ssrc}`;
1211
+ if (frame.op === 1) {
1212
+ this.voiceReceiverStreams.set(key, { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] });
1213
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, {
1214
+ userId: frame.userId,
1215
+ guildId: frame.guildId,
1216
+ ssrc: frame.ssrc,
1217
+ type: frame.type,
1218
+ timestamp: frame.timestamp,
1219
+ });
1220
+ return;
1221
+ }
1222
+ if (frame.op === 3) {
1223
+ const stream = this.voiceReceiverStreams.get(key) ?? { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] };
1224
+ stream.chunks.push(frame.data);
1225
+ this.voiceReceiverStreams.set(key, stream);
1226
+ return;
1227
+ }
1228
+ if (frame.op === 2) {
1229
+ const stream = this.voiceReceiverStreams.get(key);
1230
+ const chunks = stream?.chunks ?? [];
1231
+ if (frame.data.length > 0)
1232
+ chunks.push(frame.data);
1233
+ this.voiceReceiverStreams.delete(key);
1234
+ this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverEndSpeaking, this, {
1235
+ userId: stream?.userId ?? frame.userId,
1236
+ guildId: stream?.guildId ?? frame.guildId,
1237
+ data: Buffer.concat(chunks),
1238
+ type: stream?.type ?? frame.type,
1239
+ ssrc: frame.ssrc,
1240
+ timestamp: frame.timestamp,
1241
+ });
1242
+ }
1243
+ }
1155
1244
  /**
1156
1245
  * Handles a voice receiver error.
1157
1246
  * @param {Error} error - The error to handle.
@@ -64,8 +64,26 @@ class Rest {
64
64
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[REST] Skipping getPlayers - no session ID on node ${this.node.options.identifier}`);
65
65
  return [];
66
66
  }
67
- const result = await this.get(`/v4/sessions/${this.sessionId}/players`);
68
- return Array.isArray(result) ? result : (result?.players ?? []);
67
+ try {
68
+ const result = await this.get(`/v4/sessions/${this.sessionId}/players`);
69
+ return Array.isArray(result) ? result : (result?.players ?? []);
70
+ }
71
+ catch (err) {
72
+ const shouldFallback = err instanceof MagmastreamError_1.MagmaStreamError && err.code === Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED && err.message.includes("(400)");
73
+ if (!shouldFallback)
74
+ throw err;
75
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[REST] getPlayers bulk endpoint failed on node ${this.node.options.identifier}; falling back to individual player fetches.`);
76
+ const players = this.manager.players.filter((player) => player.node.options.identifier === this.node.options.identifier);
77
+ const results = await Promise.all(players.map(async (player) => {
78
+ try {
79
+ return await this.getPlayer(player.guildId);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }));
85
+ return results.filter((player) => player !== null);
86
+ }
69
87
  }
70
88
  /**
71
89
  * Sends a PATCH request to update player related data.
@@ -447,7 +447,7 @@ export interface TrackData {
447
447
  /** Additional track info provided by plugins. */
448
448
  pluginInfo: Record<string, unknown>;
449
449
  /** Additional track data sent back from Lavalink. */
450
- userData: Record<string, unknown>;
450
+ userData?: Record<string, unknown>;
451
451
  }
452
452
  /**
453
453
  * Playlist Raw Data
@@ -516,6 +516,15 @@ export interface TrackSearchResult {
516
516
  /** The track obtained */
517
517
  tracks: [Track];
518
518
  }
519
+ /**
520
+ * Episode Search Result
521
+ */
522
+ export interface EpisodeSearchResult {
523
+ /** The load type is always 'episode' */
524
+ loadType: LoadTypes.Episode;
525
+ /** The episode obtained */
526
+ tracks: [Track];
527
+ }
519
528
  /**
520
529
  * Search Result
521
530
  */
@@ -815,6 +824,31 @@ export interface SponsorBlockChapterStarted extends PlayerEvent {
815
824
  duration: number;
816
825
  };
817
826
  }
827
+ /**
828
+ * NodeLink SponsorBlock segment data.
829
+ */
830
+ export interface NodeLinkSponsorBlockSegment {
831
+ uuid: string;
832
+ start: number;
833
+ end: number;
834
+ category: string;
835
+ actionType: string;
836
+ votes: number;
837
+ locked: boolean;
838
+ videoDuration: number;
839
+ description: string;
840
+ }
841
+ /**
842
+ * NodeLink SponsorBlock state.
843
+ */
844
+ export interface NodeLinkSponsorBlockState {
845
+ enabled: boolean;
846
+ categories: string[];
847
+ actionTypes: string[];
848
+ segments: NodeLinkSponsorBlockSegment[];
849
+ lastSkippedUuid: string | null;
850
+ skipMarginMs: number;
851
+ }
818
852
  /**
819
853
  * SponsorBlockChaptersLoaded interface
820
854
  */
@@ -898,13 +932,21 @@ export interface LavalinkInfo {
898
932
  commit: string;
899
933
  commitTime: number;
900
934
  };
901
- jvm: string;
902
- lavaplayer: string;
935
+ jvm?: string;
936
+ lavaplayer?: string;
937
+ node?: string;
938
+ voice?: {
939
+ name: string;
940
+ version: string;
941
+ };
942
+ isNodelink?: boolean;
903
943
  sourceManagers: string[];
904
944
  filters: string[];
905
945
  plugins: {
906
946
  name: string;
907
947
  version: string;
948
+ author?: string | null;
949
+ path?: string | null;
908
950
  }[];
909
951
  }
910
952
  /**
@@ -958,6 +1000,21 @@ export interface NodeLinkGetLyricsMultiple {
958
1000
  loadType: "lyricsMultiple";
959
1001
  data: NodeLinkGetLyricsData[];
960
1002
  }
1003
+ /**
1004
+ * NodeLink Get Lyrics interface
1005
+ */
1006
+ export interface NodeLinkGetLyricsLyrics {
1007
+ loadType: "lyrics";
1008
+ data: {
1009
+ provider: string;
1010
+ lines: {
1011
+ time: number;
1012
+ duration?: number;
1013
+ text: string;
1014
+ words?: Record<string, unknown>[];
1015
+ }[];
1016
+ };
1017
+ }
961
1018
  /**
962
1019
  * NodeLink Get Lyrics Empty interface
963
1020
  */
@@ -1009,6 +1066,12 @@ export interface StartSpeakingEventVoiceReceiverData {
1009
1066
  * The guild ID of the guild where the user started speaking.
1010
1067
  */
1011
1068
  guildId: string;
1069
+ /** The SSRC identifier of the speaking user. */
1070
+ ssrc?: number;
1071
+ /** The audio frame type sent by NodeLink. */
1072
+ type?: "opus" | "pcm" | "ogg";
1073
+ /** The timestamp sent by NodeLink for the start frame. */
1074
+ timestamp?: number;
1012
1075
  }
1013
1076
  /**
1014
1077
  * End Speaking Event Voice Receiver Data interface
@@ -1025,11 +1088,15 @@ export interface EndSpeakingEventVoiceReceiverData {
1025
1088
  /**
1026
1089
  * The audio data received from the user in base64.
1027
1090
  */
1028
- data: string;
1091
+ data: string | Buffer;
1029
1092
  /**
1030
1093
  * The type of the audio data. Can be either opus or pcm. Older versions may include ogg/opus.
1031
1094
  */
1032
- type: "opus" | "pcm";
1095
+ type: "opus" | "pcm" | "ogg";
1096
+ /** The SSRC identifier of the speaking user. */
1097
+ ssrc?: number;
1098
+ /** The timestamp sent by NodeLink for the final frame. */
1099
+ timestamp?: number;
1033
1100
  }
1034
1101
  /**
1035
1102
  * Base Voice Receiver Event interface
@@ -1405,7 +1472,7 @@ export type LoadType = keyof typeof LoadTypes;
1405
1472
  /**
1406
1473
  * NodeLink Get Lyrics Enum type
1407
1474
  */
1408
- export type NodeLinkGetLyrics = NodeLinkGetLyricsSingle | NodeLinkGetLyricsMultiple | NodeLinkGetLyricsEmpty | NodeLinkGetLyricsError;
1475
+ export type NodeLinkGetLyrics = NodeLinkGetLyricsLyrics | NodeLinkGetLyricsSingle | NodeLinkGetLyricsMultiple | NodeLinkGetLyricsEmpty | NodeLinkGetLyricsError;
1409
1476
  /**
1410
1477
  * Voice Receiver Event Enum type
1411
1478
  */
@@ -1413,7 +1480,7 @@ export type VoiceReceiverEvent = StartSpeakingEventVoiceReceiver | EndSpeakingEv
1413
1480
  /**
1414
1481
  * Search Result Enum type
1415
1482
  */
1416
- export type SearchResult = TrackSearchResult | SearchSearchResult | PlaylistSearchResult | ErrorOrEmptySearchResult | AlbumSearchResult | ArtistSearchResult | StationSearchResult | PodcastSearchResult | ShowSearchResult | ShortSearchResult;
1483
+ export type SearchResult = TrackSearchResult | EpisodeSearchResult | SearchSearchResult | PlaylistSearchResult | ErrorOrEmptySearchResult | AlbumSearchResult | ArtistSearchResult | StationSearchResult | PodcastSearchResult | ShowSearchResult | ShortSearchResult;
1417
1484
  /**
1418
1485
  * Lyrics Event Enum type
1419
1486
  */
@@ -228,7 +228,14 @@ class AutoPlayUtils {
228
228
  // Iterate over autoplay platforms in order of priority
229
229
  for (const platform of autoPlaySearchPlatforms) {
230
230
  if (enabledSources.includes(platform)) {
231
- const recommendedTracks = await this.getRecommendedTracksFromSource(track, platform);
231
+ let recommendedTracks;
232
+ try {
233
+ recommendedTracks = await this.getRecommendedTracksFromSource(track, platform);
234
+ }
235
+ catch (error) {
236
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[AUTOPLAY] Failed to fetch recommendations from ${platform}: ${error instanceof Error ? error.message : String(error)}`);
237
+ continue;
238
+ }
232
239
  // If tracks are found, return them immediately
233
240
  if (recommendedTracks.length > 0) {
234
241
  return recommendedTracks;
@@ -336,7 +343,7 @@ class AutoPlayUtils {
336
343
  // return match ? match[1] : null;
337
344
  // };
338
345
  // const identifier = `sprec:seed_artists=${extractSpotifyArtistID(track.pluginInfo.artistUrl)}&seed_tracks=${track.identifier}`;
339
- const identifier = `sprec:mix:track:${track.identifier}`;
346
+ const identifier = this.manager.useableNode.isNodeLink ? `sprec:seed_tracks=${track.identifier}` : `sprec:mix:track:${track.identifier}`;
340
347
  const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
341
348
  const tracks = this.buildTracksFromResponse(recommendedResult, requester);
342
349
  return tracks;
@@ -515,6 +522,7 @@ class AutoPlayUtils {
515
522
  case Enums_1.LoadTypes.Playlist:
516
523
  return searchResult.playlist.tracks;
517
524
  case Enums_1.LoadTypes.Track:
525
+ case Enums_1.LoadTypes.Episode:
518
526
  case Enums_1.LoadTypes.Search:
519
527
  case Enums_1.LoadTypes.Short:
520
528
  return searchResult.tracks;
@@ -550,6 +558,7 @@ class AutoPlayUtils {
550
558
  case Enums_1.LoadTypes.Playlist:
551
559
  return searchResult.playlist.tracks[0] || null;
552
560
  case Enums_1.LoadTypes.Track:
561
+ case Enums_1.LoadTypes.Episode:
553
562
  case Enums_1.LoadTypes.Search:
554
563
  case Enums_1.LoadTypes.Short:
555
564
  return searchResult.tracks[0] || null;
@@ -577,6 +586,7 @@ class AutoPlayUtils {
577
586
  if (TrackUtils.isErrorOrEmptySearchResult(recommendedResult))
578
587
  return [];
579
588
  switch (recommendedResult.loadType) {
589
+ case Enums_1.LoadTypes.Episode:
580
590
  case Enums_1.LoadTypes.Track: {
581
591
  const data = recommendedResult.data;
582
592
  if (!this.isTrackData(data)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magmastream",
3
- "version": "2.10.3-alpha.5",
3
+ "version": "2.10.3-alpha.6",
4
4
  "description": "A user-friendly Lavalink client designed for NodeJS.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",