magmastream 2.10.3-dev.2 → 2.10.4

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.
@@ -370,8 +370,9 @@ class Node {
370
370
  *
371
371
  * @param request - The incoming message.
372
372
  */
373
- upgrade(request) {
374
- this.isNodeLink = this.options.isNodeLink ?? Boolean(request.headers.isnodelink) ?? false;
373
+ upgrade({ headers }) {
374
+ this.isNodeLink = Boolean(this.options.isNodeLink) || String(headers.iamnodelink ?? headers.isnodelink).includes("true");
375
+ this.rest.isNodeLink = this.isNodeLink;
375
376
  }
376
377
  /**
377
378
  * Handles the "open" event emitted by the WebSocket connection.
@@ -470,29 +471,29 @@ class Node {
470
471
  messagePayload = Buffer.concat(messagePayload);
471
472
  else if (messagePayload instanceof ArrayBuffer)
472
473
  messagePayload = Buffer.from(messagePayload);
473
- const payload = JSON.parse(messagePayload.toString());
474
- if (!payload.op)
474
+ let payload;
475
+ try {
476
+ payload = JSON.parse(messagePayload.toString());
477
+ }
478
+ catch (cause) {
479
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new MagmastreamError_1.MagmaStreamError({
480
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
481
+ message: "Received an invalid JSON gateway payload.",
482
+ cause: cause instanceof Error ? cause : undefined,
483
+ }));
484
+ return;
485
+ }
486
+ if (!payload || typeof payload !== "object" || !("op" in payload) || typeof payload.op !== "string")
475
487
  return;
476
488
  this.manager.emit(Enums_1.ManagerEventTypes.NodeRaw, payload);
477
- let player;
478
489
  switch (payload.op) {
479
490
  case "stats":
480
- delete payload.op;
481
- this.stats = { ...payload };
491
+ this.stats = this.normalizeStats(payload);
482
492
  break;
483
493
  case "playerUpdate":
484
- player = this.manager.players.get(payload.guildId);
485
- if (player && player.node.options.identifier !== this.options.identifier) {
486
- return;
487
- }
488
- if (player)
489
- player.position = payload.state.position || 0;
494
+ this.handlePlayerUpdate(payload);
490
495
  break;
491
496
  case "event":
492
- player = this.manager.players.get(payload.guildId);
493
- if (player && player.node.options.identifier !== this.options.identifier) {
494
- return;
495
- }
496
497
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Node message: ${Utils_1.JSONUtils.safe(payload, 2)}`);
497
498
  await this.handleEvent(payload);
498
499
  break;
@@ -507,6 +508,8 @@ class Node {
507
508
  this.sessionId = payload.sessionId;
508
509
  await this.updateSessionId();
509
510
  this.info = await this.fetchInfo();
511
+ this.isNodeLink = this.isNodeLink || Boolean(this.info.isNodelink);
512
+ this.rest.isNodeLink = this.isNodeLink;
510
513
  if (payload.resumed || !hadPreviousSession) {
511
514
  await this.manager.loadPlayerStates(this.options.identifier);
512
515
  }
@@ -518,10 +521,32 @@ class Node {
518
521
  }
519
522
  break;
520
523
  default:
521
- this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Unexpected op "${payload.op}" with data: ${payload.message}`));
524
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error("Unexpected gateway operation."));
522
525
  return;
523
526
  }
524
527
  }
528
+ normalizeStats(payload) {
529
+ const stats = { ...payload };
530
+ delete stats.op;
531
+ const nodelinkLoad = stats.cpu?.nodelinkLoad;
532
+ const lavalinkLoad = stats.cpu?.lavalinkLoad ?? nodelinkLoad ?? 0;
533
+ return {
534
+ ...stats,
535
+ cpu: {
536
+ ...stats.cpu,
537
+ lavalinkLoad,
538
+ ...(typeof nodelinkLoad === "number" ? { nodelinkLoad } : {}),
539
+ },
540
+ };
541
+ }
542
+ handlePlayerUpdate(payload) {
543
+ const player = this.manager.players.get(payload.guildId);
544
+ if (!player || player.node.options.identifier !== this.options.identifier)
545
+ return;
546
+ player.position = payload.state.position ?? 0;
547
+ // Transport status can lag lifecycle events; expose it without overwriting player.state.
548
+ this.manager.emit(Enums_1.ManagerEventTypes.PlayerUpdate, player, payload);
549
+ }
525
550
  /**
526
551
  * Handles an event emitted from the Lavalink node.
527
552
  * @param {PlayerEvent & PlayerEvents} payload The event emitted from the node.
@@ -529,21 +554,42 @@ class Node {
529
554
  * @private
530
555
  */
531
556
  async handleEvent(payload) {
532
- if (!payload.guildId)
557
+ if (payload.type === "ConnectionStatusEvent") {
558
+ this.markAsNodeLink();
559
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, payload);
560
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkConnectionStatus, this, payload);
533
561
  return;
562
+ }
563
+ if (payload.type === "WorkerFailedEvent") {
564
+ this.markAsNodeLink();
565
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, payload);
566
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkWorkerFailed, this, payload);
567
+ return;
568
+ }
569
+ if (!("guildId" in payload) || typeof payload.guildId !== "string") {
570
+ if (this.isNodeLink)
571
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, payload);
572
+ return;
573
+ }
534
574
  const player = this.manager.players.get(payload.guildId);
535
- if (!player)
575
+ if (!player || player.node.options.identifier !== this.options.identifier) {
576
+ if (this.isNodeLink)
577
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, payload);
536
578
  return;
537
- const track = await player.queue.getCurrent();
579
+ }
580
+ if (this.handleNodeLinkEvent(player, payload))
581
+ return;
582
+ const track = await this.resolveEventTrack(player, payload);
538
583
  const type = payload.type;
539
- const TRACK_EVENTS = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
540
- if (!track && TRACK_EVENTS.includes(type)) {
541
- this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[Node] Received ${type} for guild ${payload.guildId} but queue has no current track — ignoring.`);
584
+ const trackRequiredEvents = ["TrackStartEvent", "TrackEndEvent", "TrackStuckEvent", "TrackExceptionEvent"];
585
+ if (!track && trackRequiredEvents.includes(type)) {
586
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Received ${type} for guild ${payload.guildId}, but neither the payload nor local queue contains a usable track.`);
542
587
  return;
543
588
  }
544
- let error;
545
589
  switch (type) {
546
590
  case "TrackStartEvent":
591
+ if ((await player.queue.getCurrent())?.track !== track?.track)
592
+ await player.queue.setCurrent(track);
547
593
  this.trackStart(player, track, payload);
548
594
  break;
549
595
  case "TrackEndEvent":
@@ -566,9 +612,11 @@ class Node {
566
612
  this.socketClosed(player, payload);
567
613
  break;
568
614
  case "SegmentsLoaded":
615
+ case "SponsorBlockSegmentsLoadedEvent":
569
616
  this.sponsorBlockSegmentLoaded(player, track, payload);
570
617
  break;
571
618
  case "SegmentSkipped":
619
+ case "SponsorBlockSegmentSkippedEvent":
572
620
  this.sponsorBlockSegmentSkipped(player, track, payload);
573
621
  break;
574
622
  case "ChaptersLoaded":
@@ -587,11 +635,170 @@ class Node {
587
635
  this.lyricsLine(player, track, payload);
588
636
  break;
589
637
  default:
590
- error = new Error(`Node#event unknown event '${type}'.`);
591
- this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
638
+ if (this.isNodeLink) {
639
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, payload);
640
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Unrecognized NodeLink or plugin event '${type}' was forwarded through nodeLinkEvent.`);
641
+ return;
642
+ }
643
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Node#event unknown event '${type}'.`));
592
644
  break;
593
645
  }
594
646
  }
647
+ markAsNodeLink() {
648
+ this.isNodeLink = true;
649
+ this.rest.isNodeLink = true;
650
+ }
651
+ async resolveEventTrack(player, payload) {
652
+ const current = await player.queue.getCurrent();
653
+ const rawTrack = "track" in payload ? payload.track : null;
654
+ if (!rawTrack || typeof rawTrack !== "object" || !("encoded" in rawTrack) || !("info" in rawTrack) || typeof rawTrack.encoded !== "string")
655
+ return current;
656
+ try {
657
+ return Utils_1.TrackUtils.build(rawTrack, current?.requester, current?.isAutoplay ?? false);
658
+ }
659
+ catch (error) {
660
+ this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Failed to normalize event track for guild ${player.guildId}: ${error instanceof Error ? error.message : String(error)}`);
661
+ return current;
662
+ }
663
+ }
664
+ handleNodeLinkEvent(player, payload) {
665
+ if (!("guildId" in payload))
666
+ return false;
667
+ switch (payload.type) {
668
+ case "VolumeChangedEvent": {
669
+ this.markAsNodeLink();
670
+ const event = payload;
671
+ const oldVolume = player.volume;
672
+ const oldPlayer = { ...player };
673
+ player.volume = event.volume;
674
+ player.options.volume = event.volume;
675
+ this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
676
+ changeType: Enums_1.PlayerStateEventTypes.VolumeChange,
677
+ details: {
678
+ type: "volume",
679
+ action: "adjust",
680
+ previousVolume: oldVolume,
681
+ currentVolume: player.volume,
682
+ },
683
+ });
684
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkVolumeChanged, player, event);
685
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
686
+ return true;
687
+ }
688
+ case "PauseEvent": {
689
+ this.markAsNodeLink();
690
+ const event = payload;
691
+ const oldPaused = player.paused;
692
+ const oldPlayer = { ...player };
693
+ player.paused = event.paused;
694
+ player.playing = !event.paused;
695
+ this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
696
+ changeType: Enums_1.PlayerStateEventTypes.PauseChange,
697
+ details: {
698
+ type: "pause",
699
+ action: event.paused ? "pause" : "resume",
700
+ previousPause: oldPaused,
701
+ currentPause: player.paused,
702
+ },
703
+ });
704
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkPause, player, event);
705
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
706
+ return true;
707
+ }
708
+ case "SeekEvent": {
709
+ this.markAsNodeLink();
710
+ const event = payload;
711
+ player.position = event.position;
712
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkSeek, player, event);
713
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
714
+ return true;
715
+ }
716
+ case "FiltersChangedEvent": {
717
+ this.markAsNodeLink();
718
+ const event = payload;
719
+ player.filters.syncFromNode(event.filters);
720
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkFiltersChanged, player, event);
721
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
722
+ return true;
723
+ }
724
+ case "PlayerCreatedEvent": {
725
+ this.markAsNodeLink();
726
+ const event = payload;
727
+ player.volume = event.player.volume;
728
+ player.options.volume = event.player.volume;
729
+ player.paused = event.player.paused;
730
+ player.playing = Boolean(event.player.track) && !event.player.paused;
731
+ player.position = event.player.state.position;
732
+ player.filters.syncFromNode(event.player.filters);
733
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkPlayerCreated, player, event);
734
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
735
+ return true;
736
+ }
737
+ case "PlayerDestroyedEvent": {
738
+ this.markAsNodeLink();
739
+ const event = payload;
740
+ player.playing = false;
741
+ player.paused = false;
742
+ player.state = Enums_1.StateTypes.Disconnected;
743
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkPlayerDestroyed, player, event);
744
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
745
+ return true;
746
+ }
747
+ case "PlayerConnectedEvent": {
748
+ this.markAsNodeLink();
749
+ const event = payload;
750
+ player.state = Enums_1.StateTypes.Connected;
751
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkPlayerConnected, player, event);
752
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
753
+ return true;
754
+ }
755
+ case "PlayerReconnectingEvent": {
756
+ this.markAsNodeLink();
757
+ const event = payload;
758
+ player.state = Enums_1.StateTypes.Connecting;
759
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkPlayerReconnecting, player, event);
760
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
761
+ return true;
762
+ }
763
+ case "MixStartedEvent": {
764
+ this.markAsNodeLink();
765
+ const event = payload;
766
+ this.manager.emit(Enums_1.ManagerEventTypes.MixStarted, player, event);
767
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
768
+ return true;
769
+ }
770
+ case "MixEndedEvent": {
771
+ this.markAsNodeLink();
772
+ const event = payload;
773
+ this.manager.emit(Enums_1.ManagerEventTypes.MixEnded, player, event);
774
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
775
+ return true;
776
+ }
777
+ case "EternalBoxInfoEvent": {
778
+ this.markAsNodeLink();
779
+ const event = payload;
780
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEternalBoxInfo, player, event);
781
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
782
+ return true;
783
+ }
784
+ case "EternalBoxJumpEvent": {
785
+ this.markAsNodeLink();
786
+ const event = payload;
787
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEternalBoxJump, player, event);
788
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
789
+ return true;
790
+ }
791
+ case "StreamMetadataEvent": {
792
+ this.markAsNodeLink();
793
+ const event = payload;
794
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkStreamMetadata, player, event);
795
+ this.manager.emit(Enums_1.ManagerEventTypes.NodeLinkEvent, this, event);
796
+ return true;
797
+ }
798
+ default:
799
+ return false;
800
+ }
801
+ }
595
802
  /**
596
803
  * Emitted when a new track starts playing.
597
804
  * @param {Player} player The player that started playing the track.
@@ -600,7 +807,7 @@ class Node {
600
807
  * @private
601
808
  */
602
809
  trackStart(player, track, payload) {
603
- const oldPlayer = player;
810
+ const oldPlayer = { ...player };
604
811
  player.playing = true;
605
812
  player.paused = false;
606
813
  this.manager.emit(Enums_1.ManagerEventTypes.TrackStart, player, track, payload);
@@ -641,7 +848,7 @@ class Node {
641
848
  await player.queue.addPrevious(current);
642
849
  }
643
850
  player.set("skipFlag", false);
644
- const oldPlayer = player;
851
+ const oldPlayer = { ...player };
645
852
  switch (reason) {
646
853
  case Enums_1.TrackEndReasonTypes.LoadFailed:
647
854
  case Enums_1.TrackEndReasonTypes.Cleanup:
@@ -671,6 +878,12 @@ class Node {
671
878
  await this.queueEnd(player, track, payload);
672
879
  }
673
880
  break;
881
+ case Enums_1.TrackEndReasonTypes.Gapless:
882
+ await player.queue.setCurrent((await player.queue.dequeue()) ?? null);
883
+ player.playing = true;
884
+ player.paused = false;
885
+ this.manager.emit(Enums_1.ManagerEventTypes.TrackEnd, player, track, payload);
886
+ break;
674
887
  default:
675
888
  this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, new Error(`Unexpected track end reason "${reason}"`));
676
889
  break;
@@ -849,7 +1062,7 @@ class Node {
849
1062
  });
850
1063
  }
851
1064
  if (this.isNodeLink) {
852
- return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&language=${language}` : ""}`));
1065
+ return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&lang=${encodeURIComponent(language)}` : ""}`));
853
1066
  }
854
1067
  const requiredPlugins = ["lavalyrics-plugin"];
855
1068
  for (const plugin of requiredPlugins) {
@@ -871,6 +1084,41 @@ class Node {
871
1084
  const lyrics = (await this.rest.get(`/v4/lyrics?track=${encodeURIComponent(track.track)}&skipTrackSource=${skipTrackSource}`));
872
1085
  return lyrics || { source: null, provider: null, text: null, lines: [], plugin: [] };
873
1086
  }
1087
+ /**
1088
+ * Loads chapters for a track using NodeLink.
1089
+ * @param {Track} track - The track to load chapters for.
1090
+ * @returns {Promise<NodeLinkChapter[]>} A promise that resolves with the chapter list.
1091
+ */
1092
+ async getChapters(track) {
1093
+ if (!this.connected) {
1094
+ throw new MagmastreamError_1.MagmaStreamError({
1095
+ code: Enums_1.MagmaStreamErrorCode.NODE_DISCONNECTED,
1096
+ message: `The node is not connected to the lavalink server.`,
1097
+ context: { identifier: this.options.identifier },
1098
+ });
1099
+ }
1100
+ if (!this.isNodeLink) {
1101
+ throw new MagmastreamError_1.MagmaStreamError({
1102
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1103
+ message: `The node is not a NodeLink, cannot load chapters.`,
1104
+ context: { identifier: this.options.identifier },
1105
+ });
1106
+ }
1107
+ return (await this.rest.get(`/v4/loadchapters?encodedTrack=${encodeURIComponent(track.track)}`));
1108
+ }
1109
+ /**
1110
+ * Loads chapters for a track using NodeLink.
1111
+ * @param {Track} track - The track to load chapters for.
1112
+ * @returns {Promise<NodeLinkChapter[]>} A promise that resolves with the chapter list.
1113
+ */
1114
+ async getNodeLinkChapters(track) {
1115
+ return this.getChapters(track);
1116
+ }
1117
+ /** Loads NodeLink meaning/background information for a track. */
1118
+ async getNodeLinkMeaning(track, language = "en") {
1119
+ this.assertNodeLinkFeature("load track meaning");
1120
+ return this.rest.getNodeLinkMeaning(track.track, language);
1121
+ }
874
1122
  /**
875
1123
  * Subscribes to lyrics for a player.
876
1124
  * @param {string} guildId - The ID of the guild to subscribe to lyrics for.
@@ -886,13 +1134,8 @@ class Node {
886
1134
  context: { identifier: this.options.identifier },
887
1135
  });
888
1136
  }
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
- }
1137
+ if (this.isNodeLink)
1138
+ return await this.rest.post(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource}`, {});
896
1139
  const requiredPlugins = ["lavalyrics-plugin"];
897
1140
  for (const plugin of requiredPlugins) {
898
1141
  if (!this.info.plugins.some((p) => p.name === plugin)) {
@@ -938,13 +1181,8 @@ class Node {
938
1181
  context: { identifier: this.options.identifier },
939
1182
  });
940
1183
  }
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
- }
1184
+ if (this.isNodeLink)
1185
+ return await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe`);
948
1186
  if (!this.info.plugins.some((plugin) => plugin.name === "java-lyrics-plugin")) {
949
1187
  throw new MagmastreamError_1.MagmaStreamError({
950
1188
  code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
@@ -1001,6 +1239,7 @@ class Node {
1001
1239
  * @param {WebSocketClosedEvent} payload - The event payload containing additional data about the WebSocket close event.
1002
1240
  */
1003
1241
  socketClosed(player, payload) {
1242
+ player.state = Enums_1.StateTypes.Disconnected;
1004
1243
  this.manager.emit(Enums_1.ManagerEventTypes.SocketClosed, player, payload);
1005
1244
  this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] Websocket closed for player: ${player.guildId} with payload: ${Utils_1.JSONUtils.safe(payload, 2)}`);
1006
1245
  }
@@ -1079,7 +1318,7 @@ class Node {
1079
1318
  * @returns {Promise<LavalinkInfo>} A promise that resolves to the Lavalink node information.
1080
1319
  */
1081
1320
  async fetchInfo() {
1082
- return (await this.rest.get(`/v4/info`));
1321
+ return await this.rest.getInfo();
1083
1322
  }
1084
1323
  /**
1085
1324
  * Gets the current sponsorblock segments for a player.
@@ -1088,6 +1327,22 @@ class Node {
1088
1327
  * @throws {RangeError} If the sponsorblock-plugin is not available in the Lavalink node.
1089
1328
  */
1090
1329
  async getSponsorBlock(player) {
1330
+ if (this.isNodeLink) {
1331
+ try {
1332
+ const state = (await this.rest.get(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`));
1333
+ return state.categories.map((segment) => segment);
1334
+ }
1335
+ catch (err) {
1336
+ throw err instanceof MagmastreamError_1.MagmaStreamError
1337
+ ? err
1338
+ : new MagmastreamError_1.MagmaStreamError({
1339
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1340
+ message: "Failed to fetch NodeLink SponsorBlock state.",
1341
+ cause: err instanceof Error ? err : undefined,
1342
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1343
+ });
1344
+ }
1345
+ }
1091
1346
  if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1092
1347
  throw new MagmastreamError_1.MagmaStreamError({
1093
1348
  code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
@@ -1109,6 +1364,166 @@ class Node {
1109
1364
  });
1110
1365
  }
1111
1366
  }
1367
+ /** Returns the complete NodeLink SponsorBlock state for a player. */
1368
+ async getNodeLinkSponsorBlockState(player) {
1369
+ this.assertNodeLinkFeature("get SponsorBlock state");
1370
+ return await this.rest.get(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`);
1371
+ }
1372
+ /** Updates NodeLink SponsorBlock behavior for a player. */
1373
+ async updateNodeLinkSponsorBlock(player, update) {
1374
+ this.assertNodeLinkFeature("update SponsorBlock state");
1375
+ this.assertNodeLinkSponsorBlockUpdate(update);
1376
+ return await this.rest.patch(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`, update);
1377
+ }
1378
+ /** Replaces the NodeLink SponsorBlock segment list for a player. */
1379
+ async setNodeLinkSponsorBlockSegments(player, segments) {
1380
+ this.assertNodeLinkFeature("set SponsorBlock segments");
1381
+ if (!Array.isArray(segments) || !segments.every((segment) => this.isValidNodeLinkSponsorBlockSegment(segment))) {
1382
+ throw new MagmastreamError_1.MagmaStreamError({
1383
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1384
+ message: "NodeLink SponsorBlock segments must match the NodeLink segment schema.",
1385
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1386
+ });
1387
+ }
1388
+ return await this.rest.post(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`, { segments });
1389
+ }
1390
+ /**
1391
+ * Adds a NodeLink audio mix layer to the player.
1392
+ * @param {Player} player - The player to add the mix to.
1393
+ * @param {Track} track - The track to mix over the current playback.
1394
+ * @param {number} [volume] - The mix volume in the 0-1 range.
1395
+ * @returns {Promise<NodeLinkMix>} The created mix layer.
1396
+ */
1397
+ async addMix(player, track, volume) {
1398
+ this.assertNodeLinkFeature("add an audio mix layer");
1399
+ this.assertMixVolume(volume);
1400
+ return (await this.rest.post(`/v4/sessions/${this.sessionId}/players/${player.guildId}/mix`, {
1401
+ track: { encoded: track.track },
1402
+ ...(typeof volume === "number" && { volume }),
1403
+ }));
1404
+ }
1405
+ /**
1406
+ * Adds a NodeLink audio mix layer to the player.
1407
+ * @param {Player} player - The player to add the mix to.
1408
+ * @param {Track} track - The track to mix over the current playback.
1409
+ * @param {number} [volume] - The mix volume in the 0-1 range.
1410
+ * @returns {Promise<NodeLinkMix>} The created mix layer.
1411
+ */
1412
+ async addNodeLinkMix(player, track, volume) {
1413
+ return this.addMix(player, track, volume);
1414
+ }
1415
+ /**
1416
+ * Gets the active NodeLink audio mix layers for the player.
1417
+ * @param {Player} player - The player to fetch mixes for.
1418
+ * @returns {Promise<NodeLinkMix[]>} The active mix layers.
1419
+ */
1420
+ async getMixes(player) {
1421
+ this.assertNodeLinkFeature("get audio mix layers");
1422
+ const response = (await this.rest.get(`/v4/sessions/${this.sessionId}/players/${player.guildId}/mix`));
1423
+ return response.mixes ?? [];
1424
+ }
1425
+ /**
1426
+ * Gets the active NodeLink audio mix layers for the player.
1427
+ * @param {Player} player - The player to fetch mixes for.
1428
+ * @returns {Promise<NodeLinkMix[]>} The active mix layers.
1429
+ */
1430
+ async getNodeLinkMixes(player) {
1431
+ return this.getMixes(player);
1432
+ }
1433
+ /**
1434
+ * Updates a NodeLink audio mix layer volume.
1435
+ * @param {Player} player - The player that owns the mix.
1436
+ * @param {string} mixId - The mix layer id.
1437
+ * @param {number} volume - The mix volume in the 0-1 range.
1438
+ * @returns {Promise<void>}
1439
+ */
1440
+ async updateMix(player, mixId, volume) {
1441
+ this.assertNodeLinkFeature("update an audio mix layer");
1442
+ this.assertMixVolume(volume);
1443
+ await this.rest.patch(`/v4/sessions/${this.sessionId}/players/${player.guildId}/mix/${encodeURIComponent(mixId)}`, { volume });
1444
+ }
1445
+ /**
1446
+ * Updates a NodeLink audio mix layer volume.
1447
+ * @param {Player} player - The player that owns the mix.
1448
+ * @param {string} mixId - The mix layer id.
1449
+ * @param {number} volume - The mix volume in the 0-1 range.
1450
+ * @returns {Promise<void>}
1451
+ */
1452
+ async updateNodeLinkMix(player, mixId, volume) {
1453
+ return this.updateMix(player, mixId, volume);
1454
+ }
1455
+ /**
1456
+ * Removes a NodeLink audio mix layer.
1457
+ * @param {Player} player - The player that owns the mix.
1458
+ * @param {string} mixId - The mix layer id.
1459
+ * @returns {Promise<void>}
1460
+ */
1461
+ async removeMix(player, mixId) {
1462
+ this.assertNodeLinkFeature("remove an audio mix layer");
1463
+ await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${player.guildId}/mix/${encodeURIComponent(mixId)}`);
1464
+ }
1465
+ /**
1466
+ * Removes a NodeLink audio mix layer.
1467
+ * @param {Player} player - The player that owns the mix.
1468
+ * @param {string} mixId - The mix layer id.
1469
+ * @returns {Promise<void>}
1470
+ */
1471
+ async removeNodeLinkMix(player, mixId) {
1472
+ return this.removeMix(player, mixId);
1473
+ }
1474
+ assertNodeLinkFeature(action) {
1475
+ if (this.isNodeLink && this.connected)
1476
+ return;
1477
+ throw new MagmastreamError_1.MagmaStreamError({
1478
+ code: this.connected ? Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR : Enums_1.MagmaStreamErrorCode.NODE_DISCONNECTED,
1479
+ message: this.connected ? `The node is not a NodeLink, cannot ${action}.` : `The node is not connected to the lavalink server.`,
1480
+ context: { identifier: this.options.identifier },
1481
+ });
1482
+ }
1483
+ assertMixVolume(volume) {
1484
+ if (volume === undefined)
1485
+ return;
1486
+ if (typeof volume === "number" && Number.isFinite(volume) && volume >= 0 && volume <= 1)
1487
+ return;
1488
+ throw new MagmastreamError_1.MagmaStreamError({
1489
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1490
+ message: "NodeLink mix volume must be between 0 and 1.",
1491
+ context: { identifier: this.options.identifier, volume },
1492
+ });
1493
+ }
1494
+ assertNodeLinkSponsorBlockUpdate(update) {
1495
+ const valid = update !== null &&
1496
+ typeof update === "object" &&
1497
+ (update.enabled === undefined || typeof update.enabled === "boolean") &&
1498
+ (update.categories === undefined || (Array.isArray(update.categories) && update.categories.every((value) => typeof value === "string"))) &&
1499
+ (update.actionTypes === undefined || (Array.isArray(update.actionTypes) && update.actionTypes.every((value) => typeof value === "string"))) &&
1500
+ (update.skipMarginMs === undefined || (typeof update.skipMarginMs === "number" && Number.isFinite(update.skipMarginMs) && update.skipMarginMs >= 0));
1501
+ if (valid)
1502
+ return;
1503
+ throw new MagmastreamError_1.MagmaStreamError({
1504
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1505
+ message: "Invalid NodeLink SponsorBlock update payload.",
1506
+ context: { identifier: this.options.identifier, update },
1507
+ });
1508
+ }
1509
+ isValidNodeLinkSponsorBlockSegment(segment) {
1510
+ return (segment !== null &&
1511
+ typeof segment === "object" &&
1512
+ typeof segment.uuid === "string" &&
1513
+ typeof segment.start === "number" &&
1514
+ Number.isFinite(segment.start) &&
1515
+ typeof segment.end === "number" &&
1516
+ Number.isFinite(segment.end) &&
1517
+ segment.end >= segment.start &&
1518
+ typeof segment.category === "string" &&
1519
+ typeof segment.actionType === "string" &&
1520
+ typeof segment.votes === "number" &&
1521
+ Number.isFinite(segment.votes) &&
1522
+ typeof segment.locked === "boolean" &&
1523
+ typeof segment.videoDuration === "number" &&
1524
+ Number.isFinite(segment.videoDuration) &&
1525
+ typeof segment.description === "string");
1526
+ }
1112
1527
  /**
1113
1528
  * Sets the sponsorblock segments for a player.
1114
1529
  * @param {Player} player - The player to set the sponsor blocks for.
@@ -1124,13 +1539,6 @@ class Node {
1124
1539
  * ```
1125
1540
  */
1126
1541
  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
1542
  if (!segments.length) {
1135
1543
  throw new MagmastreamError_1.MagmaStreamError({
1136
1544
  code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
@@ -1145,8 +1553,35 @@ class Node {
1145
1553
  context: { identifier: this.options.identifier, guildId: player.guildId, invalidSegments: segments },
1146
1554
  });
1147
1555
  }
1556
+ const categories = segments.map((segment) => segment.toLowerCase());
1557
+ if (this.isNodeLink) {
1558
+ try {
1559
+ await this.rest.patch(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`, {
1560
+ enabled: true,
1561
+ categories,
1562
+ });
1563
+ return;
1564
+ }
1565
+ catch (err) {
1566
+ throw err instanceof MagmastreamError_1.MagmaStreamError
1567
+ ? err
1568
+ : new MagmastreamError_1.MagmaStreamError({
1569
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1570
+ message: "Failed to set NodeLink SponsorBlock categories.",
1571
+ cause: err instanceof Error ? err : undefined,
1572
+ context: { identifier: this.options.identifier, guildId: player.guildId, segments },
1573
+ });
1574
+ }
1575
+ }
1576
+ if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1577
+ throw new MagmastreamError_1.MagmaStreamError({
1578
+ code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
1579
+ message: `The plugin "sponsorblock-plugin" must be present in the lavalink node to set SponsorBlock segments.`,
1580
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1581
+ });
1582
+ }
1148
1583
  try {
1149
- await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, segments.map((segment) => segment.toLowerCase()));
1584
+ await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, categories);
1150
1585
  }
1151
1586
  catch (err) {
1152
1587
  throw err instanceof MagmastreamError_1.MagmaStreamError
@@ -1166,6 +1601,22 @@ class Node {
1166
1601
  * @throws {RangeError} If the sponsorblock-plugin is not available in the Lavalink node.
1167
1602
  */
1168
1603
  async deleteSponsorBlock(player) {
1604
+ if (this.isNodeLink) {
1605
+ try {
1606
+ await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`);
1607
+ return;
1608
+ }
1609
+ catch (err) {
1610
+ throw err instanceof MagmastreamError_1.MagmaStreamError
1611
+ ? err
1612
+ : new MagmastreamError_1.MagmaStreamError({
1613
+ code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
1614
+ message: "Failed to delete NodeLink SponsorBlock state.",
1615
+ cause: err instanceof Error ? err : undefined,
1616
+ context: { identifier: this.options.identifier, guildId: player.guildId },
1617
+ });
1618
+ }
1619
+ }
1169
1620
  if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
1170
1621
  throw new MagmastreamError_1.MagmaStreamError({
1171
1622
  code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,