magmastream 2.10.3-alpha.4 → 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",
@@ -250,10 +252,14 @@ export declare enum AvailableFilters {
250
252
  Party = "party",
251
253
  Pop = "pop",
252
254
  Radio = "radio",
255
+ PluginFilters = "pluginFilters",
256
+ SetChannelMix = "setChannelMix",
253
257
  SetDistortion = "setDistortion",
254
258
  SetKaraoke = "setKaraoke",
259
+ SetLowPass = "setLowPass",
255
260
  SetRotation = "setRotation",
256
261
  SetTimescale = "setTimescale",
262
+ SetTremolo = "setTremolo",
257
263
  Slowmo = "slowmo",
258
264
  Soft = "soft",
259
265
  TrebleBass = "trebleBass",
@@ -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";
@@ -267,10 +269,14 @@ var AvailableFilters;
267
269
  AvailableFilters["Party"] = "party";
268
270
  AvailableFilters["Pop"] = "pop";
269
271
  AvailableFilters["Radio"] = "radio";
272
+ AvailableFilters["PluginFilters"] = "pluginFilters";
273
+ AvailableFilters["SetChannelMix"] = "setChannelMix";
270
274
  AvailableFilters["SetDistortion"] = "setDistortion";
271
275
  AvailableFilters["SetKaraoke"] = "setKaraoke";
276
+ AvailableFilters["SetLowPass"] = "setLowPass";
272
277
  AvailableFilters["SetRotation"] = "setRotation";
273
278
  AvailableFilters["SetTimescale"] = "setTimescale";
279
+ AvailableFilters["SetTremolo"] = "setTremolo";
274
280
  AvailableFilters["Slowmo"] = "slowmo";
275
281
  AvailableFilters["Soft"] = "soft";
276
282
  AvailableFilters["TrebleBass"] = "trebleBass";
@@ -2,14 +2,19 @@ import { Band } from "../utils/filtersEqualizers";
2
2
  import { AvailableFilters } from "./Enums";
3
3
  import { Manager } from "./Manager";
4
4
  import { Player } from "./Player";
5
- import { DistortionOptions, KaraokeOptions, ReverbOptions, RotationOptions, TimescaleOptions, VibratoOptions } from "./Types";
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;
10
11
  rotation: RotationOptions | null;
11
12
  timescale: TimescaleOptions | null;
13
+ tremoloOptions: TremoloOptions | null;
12
14
  vibrato: VibratoOptions | null;
15
+ channelMix: ChannelMixOptions | null;
16
+ lowPass: LowPassOptions | null;
17
+ pluginFilters: Record<string, unknown>;
13
18
  reverb: ReverbOptions | null;
14
19
  volume: number;
15
20
  bassBoostlevel: number;
@@ -42,6 +47,7 @@ export declare class Filters {
42
47
  */
43
48
  private applyFilter;
44
49
  private emitPlayersTasteUpdate;
50
+ private assertPluginFiltersSupported;
45
51
  /**
46
52
  * Sets the status of a specific filter.
47
53
  *
@@ -112,6 +118,35 @@ export declare class Filters {
112
118
  * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
113
119
  */
114
120
  setVibrato(vibrato?: VibratoOptions): Promise<this>;
121
+ /**
122
+ * Sets the tremolo options on the audio.
123
+ *
124
+ * @param {TremoloOptions} [tremolo] - The tremolo settings to apply (frequency, depth).
125
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
126
+ */
127
+ setTremolo(tremolo?: TremoloOptions): Promise<this>;
128
+ /**
129
+ * Sets the stereo channel mix options on the audio.
130
+ *
131
+ * @param {ChannelMixOptions} [channelMix] - The channel mix settings to apply.
132
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
133
+ */
134
+ setChannelMix(channelMix?: ChannelMixOptions): Promise<this>;
135
+ /**
136
+ * Sets the low pass filter options on the audio.
137
+ *
138
+ * @param {LowPassOptions} [lowPass] - The low pass settings to apply.
139
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
140
+ */
141
+ setLowPass(lowPass?: LowPassOptions): Promise<this>;
142
+ /**
143
+ * Sets plugin-provided filter options on the audio.
144
+ *
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.
147
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
148
+ */
149
+ setPluginFilters(pluginFilters?: Record<string, unknown>, requiredPluginNames?: string | string[]): Promise<this>;
115
150
  /**
116
151
  * Sets the own rotation options effect to the audio.
117
152
  *
@@ -5,12 +5,17 @@ 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;
11
12
  rotation;
12
13
  timescale;
14
+ tremoloOptions;
13
15
  vibrato;
16
+ channelMix;
17
+ lowPass;
18
+ pluginFilters;
14
19
  reverb;
15
20
  volume;
16
21
  bassBoostlevel;
@@ -23,7 +28,11 @@ class Filters {
23
28
  this.karaoke = null;
24
29
  this.rotation = null;
25
30
  this.timescale = null;
31
+ this.tremoloOptions = null;
26
32
  this.vibrato = null;
33
+ this.channelMix = null;
34
+ this.lowPass = null;
35
+ this.pluginFilters = {};
27
36
  this.reverb = null;
28
37
  this.volume = 1.0;
29
38
  this.bassBoostlevel = 0;
@@ -48,7 +57,7 @@ class Filters {
48
57
  * of the Filters class for method chaining.
49
58
  */
50
59
  async updateFilters() {
51
- const { distortion, equalizer, karaoke, rotation, timescale, vibrato, volume } = this;
60
+ const { channelMix, distortion, equalizer, karaoke, lowPass, pluginFilters, rotation, timescale, tremoloOptions, vibrato, volume } = this;
52
61
  try {
53
62
  await this.player.node.rest.updatePlayer({
54
63
  data: {
@@ -56,10 +65,14 @@ class Filters {
56
65
  distortion,
57
66
  equalizer,
58
67
  karaoke,
68
+ lowPass,
69
+ pluginFilters,
59
70
  rotation,
60
71
  timescale,
72
+ tremolo: tremoloOptions,
61
73
  vibrato,
62
74
  volume,
75
+ channelMix,
63
76
  },
64
77
  },
65
78
  guildId: this.player.guildId,
@@ -101,6 +114,18 @@ class Filters {
101
114
  details: { action: "change" },
102
115
  });
103
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
+ }
104
129
  /**
105
130
  * Sets the status of a specific filter.
106
131
  *
@@ -146,7 +171,11 @@ class Filters {
146
171
  await this.setKaraoke(null);
147
172
  await this.setRotation(null);
148
173
  await this.setTimescale(null);
174
+ await this.setTremolo(null);
149
175
  await this.setVibrato(null);
176
+ await this.setChannelMix(null);
177
+ await this.setLowPass(null);
178
+ await this.setPluginFilters({});
150
179
  await this.updateFilters();
151
180
  this.emitPlayersTasteUpdate(oldPlayer);
152
181
  return this;
@@ -215,6 +244,63 @@ class Filters {
215
244
  this.emitPlayersTasteUpdate(oldPlayer);
216
245
  return this;
217
246
  }
247
+ /**
248
+ * Sets the tremolo options on the audio.
249
+ *
250
+ * @param {TremoloOptions} [tremolo] - The tremolo settings to apply (frequency, depth).
251
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
252
+ */
253
+ async setTremolo(tremolo) {
254
+ const oldPlayer = { ...this };
255
+ await this.applyFilter({ property: "tremoloOptions", value: tremolo ?? null });
256
+ this.setFilterStatus(Enums_1.AvailableFilters.SetTremolo, !!tremolo);
257
+ this.emitPlayersTasteUpdate(oldPlayer);
258
+ return this;
259
+ }
260
+ /**
261
+ * Sets the stereo channel mix options on the audio.
262
+ *
263
+ * @param {ChannelMixOptions} [channelMix] - The channel mix settings to apply.
264
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
265
+ */
266
+ async setChannelMix(channelMix) {
267
+ const oldPlayer = { ...this };
268
+ await this.applyFilter({ property: "channelMix", value: channelMix ?? null });
269
+ this.setFilterStatus(Enums_1.AvailableFilters.SetChannelMix, !!channelMix);
270
+ this.emitPlayersTasteUpdate(oldPlayer);
271
+ return this;
272
+ }
273
+ /**
274
+ * Sets the low pass filter options on the audio.
275
+ *
276
+ * @param {LowPassOptions} [lowPass] - The low pass settings to apply.
277
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
278
+ */
279
+ async setLowPass(lowPass) {
280
+ const oldPlayer = { ...this };
281
+ await this.applyFilter({ property: "lowPass", value: lowPass ?? null });
282
+ this.setFilterStatus(Enums_1.AvailableFilters.SetLowPass, !!lowPass);
283
+ this.emitPlayersTasteUpdate(oldPlayer);
284
+ return this;
285
+ }
286
+ /**
287
+ * Sets plugin-provided filter options on the audio.
288
+ *
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.
291
+ * @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
292
+ */
293
+ async setPluginFilters(pluginFilters, requiredPluginNames = Filters.defaultPluginFilterPlugins) {
294
+ const oldPlayer = { ...this };
295
+ const nextPluginFilters = pluginFilters ?? {};
296
+ if (Object.keys(nextPluginFilters).length > 0) {
297
+ this.assertPluginFiltersSupported(requiredPluginNames);
298
+ }
299
+ await this.applyFilter({ property: "pluginFilters", value: nextPluginFilters });
300
+ this.setFilterStatus(Enums_1.AvailableFilters.PluginFilters, Object.keys(nextPluginFilters).length > 0);
301
+ this.emitPlayersTasteUpdate(oldPlayer);
302
+ return this;
303
+ }
218
304
  /**
219
305
  * Sets the own rotation options effect to the audio.
220
306
  *
@@ -557,7 +643,7 @@ class Filters {
557
643
  */
558
644
  async tremolo(status) {
559
645
  const oldPlayer = { ...this };
560
- await this.applyFilter({ property: "vibrato", value: status ? { frequency: 5, depth: 0.5 } : null });
646
+ await this.applyFilter({ property: "tremoloOptions", value: status ? { frequency: 5, depth: 0.5 } : null });
561
647
  this.setFilterStatus(Enums_1.AvailableFilters.Tremolo, status);
562
648
  this.emitPlayersTasteUpdate(oldPlayer);
563
649
  return this;
@@ -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);
@@ -250,7 +251,8 @@ class Manager extends events_1.EventEmitter {
250
251
  tracks,
251
252
  playlist: {
252
253
  name: playlistData.info.name,
253
- playlistInfo: playlistData.pluginInfo,
254
+ playlistInfo: playlistData.info,
255
+ pluginInfo: playlistData.pluginInfo,
254
256
  requester: requester,
255
257
  tracks,
256
258
  duration: tracks.reduce((acc, cur) => acc + (cur.duration || 0), 0),
@@ -263,7 +265,7 @@ class Manager extends events_1.EventEmitter {
263
265
  }
264
266
  if (this.options.normalizeYouTubeTitles && "tracks" in result) {
265
267
  const processTrack = (track) => {
266
- if (!YOUTUBE_URL_PATTERN.test(track.uri))
268
+ if (!track.uri || !YOUTUBE_URL_PATTERN.test(track.uri))
267
269
  return track;
268
270
  const { cleanTitle, cleanAuthor } = this.parseYouTubeTitle(track.title, track.author);
269
271
  track.title = cleanTitle;
@@ -406,23 +408,21 @@ class Manager extends events_1.EventEmitter {
406
408
  */
407
409
  async decodeTracks(tracks) {
408
410
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Decoding tracks: ${Utils_1.JSONUtils.safe(tracks, 2)}`);
409
- return new Promise(async (resolve, reject) => {
410
- const node = this.useableNode;
411
- if (!node) {
412
- throw new MagmastreamError_1.MagmaStreamError({
413
- code: Enums_1.MagmaStreamErrorCode.MANAGER_NO_NODES,
414
- message: "No available nodes to decode tracks.",
415
- });
416
- }
417
- const decodedTrackData = (await node.rest.post("/v4/decodetracks", Utils_1.JSONUtils.safe(tracks, 2)).catch((err) => reject(err)));
418
- if (!decodedTrackData) {
419
- throw new MagmastreamError_1.MagmaStreamError({
420
- code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
421
- message: "No decoded tracks returned from node.",
422
- });
423
- }
424
- return resolve(decodedTrackData);
425
- });
411
+ const node = this.useableNode;
412
+ if (!node) {
413
+ throw new MagmastreamError_1.MagmaStreamError({
414
+ code: Enums_1.MagmaStreamErrorCode.MANAGER_NO_NODES,
415
+ message: "No available nodes to decode tracks.",
416
+ });
417
+ }
418
+ const decodedTrackData = await node.rest.decodeTracks(tracks);
419
+ if (!decodedTrackData) {
420
+ throw new MagmastreamError_1.MagmaStreamError({
421
+ code: Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED,
422
+ message: "No decoded tracks returned from node.",
423
+ });
424
+ }
425
+ return decodedTrackData;
426
426
  }
427
427
  /**
428
428
  * Decodes a base64 encoded track and returns a TrackData.
@@ -510,7 +510,7 @@ class Manager extends events_1.EventEmitter {
510
510
  const player = this.create(playerOptions);
511
511
  this.emit(Enums_1.ManagerEventTypes.Debug, `[MANAGER] Recreating player: ${guildId}`);
512
512
  if (state.isAutoplay) {
513
- const savedUser = state.data?.clientUser;
513
+ const savedUser = (state.data?.Internal_AutoplayUser || state.data?.clientUser);
514
514
  if (savedUser) {
515
515
  const autoPlayUser = await player.manager.resolveUser(savedUser);
516
516
  player.setAutoplay(true, autoPlayUser, state.autoplayTries);
@@ -607,14 +607,18 @@ class Manager extends events_1.EventEmitter {
607
607
  const filterActions = {
608
608
  bassboost: () => player.filters.bassBoost(state.filters.bassBoostlevel),
609
609
  distort: (isEnabled) => player.filters.distort(isEnabled),
610
+ pluginFilters: () => player.filters.setPluginFilters(state.filters.pluginFilters),
611
+ setChannelMix: () => player.filters.setChannelMix(state.filters.channelMix),
610
612
  setDistortion: () => player.filters.setDistortion(state.filters.distortion),
611
613
  eightD: (isEnabled) => player.filters.eightD(isEnabled),
612
614
  setKaraoke: () => player.filters.setKaraoke(state.filters.karaoke),
615
+ setLowPass: () => player.filters.setLowPass(state.filters.lowPass),
613
616
  nightcore: (isEnabled) => player.filters.nightcore(isEnabled),
614
617
  slowmo: (isEnabled) => player.filters.slowmo(isEnabled),
615
618
  soft: (isEnabled) => player.filters.soft(isEnabled),
616
619
  trebleBass: (isEnabled) => player.filters.trebleBass(isEnabled),
617
620
  setTimescale: () => player.filters.setTimescale(state.filters.timescale),
621
+ setTremolo: () => player.filters.setTremolo(state.filters.tremolo),
618
622
  tv: (isEnabled) => player.filters.tv(isEnabled),
619
623
  vibrato: () => player.filters.setVibrato(state.filters.vibrato),
620
624
  vaporwave: (isEnabled) => player.filters.vaporwave(isEnabled),
@@ -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,
@@ -1079,7 +1134,7 @@ class Node {
1079
1134
  * @returns {Promise<LavalinkInfo>} A promise that resolves to the Lavalink node information.
1080
1135
  */
1081
1136
  async fetchInfo() {
1082
- return (await this.rest.get(`/v4/info`));
1137
+ return await this.rest.getInfo();
1083
1138
  }
1084
1139
  /**
1085
1140
  * Gets the current sponsorblock segments for a player.
@@ -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.