magmastream 2.10.3-dev.2 → 2.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/structures/Enums.d.ts +145 -1
- package/dist/structures/Enums.js +148 -1
- package/dist/structures/Filters.d.ts +120 -1
- package/dist/structures/Filters.js +348 -11
- package/dist/structures/Manager.d.ts +9 -0
- package/dist/structures/Manager.js +169 -30
- package/dist/structures/Node.d.ts +89 -2
- package/dist/structures/Node.js +504 -53
- package/dist/structures/Player.d.ts +85 -2
- package/dist/structures/Player.js +300 -12
- package/dist/structures/Rest.d.ts +61 -2
- package/dist/structures/Rest.js +210 -1
- package/dist/structures/Types.d.ts +645 -90
- package/dist/structures/Utils.d.ts +1 -1
- package/dist/structures/Utils.js +60 -18
- package/dist/utils/managerCheck.js +3 -2
- package/package.json +1 -1
package/dist/structures/Node.js
CHANGED
|
@@ -370,8 +370,9 @@ class Node {
|
|
|
370
370
|
*
|
|
371
371
|
* @param request - The incoming message.
|
|
372
372
|
*/
|
|
373
|
-
upgrade(
|
|
374
|
-
this.isNodeLink = this.options.isNodeLink
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
481
|
-
this.stats = { ...payload };
|
|
491
|
+
this.stats = this.normalizeStats(payload);
|
|
482
492
|
break;
|
|
483
493
|
case "playerUpdate":
|
|
484
|
-
|
|
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(
|
|
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
|
+
player.state = payload.state.connected ? Enums_1.StateTypes.Connected : Enums_1.StateTypes.Disconnected;
|
|
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 (
|
|
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
|
-
|
|
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
|
|
540
|
-
if (!track &&
|
|
541
|
-
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[
|
|
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
|
-
|
|
591
|
-
|
|
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 ? `&
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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`,
|
|
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,
|