lavalink-client 1.1.25 → 1.2.1
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/README.md +39 -1
- package/dist/cjs/structures/CustomSearches/BandCampSearch.js +1 -0
- package/dist/cjs/structures/LavalinkManager.d.ts +51 -12
- package/dist/cjs/structures/LavalinkManager.js +21 -14
- package/dist/cjs/structures/Node.d.ts +11 -2
- package/dist/cjs/structures/Node.js +111 -32
- package/dist/cjs/structures/Player.d.ts +15 -2
- package/dist/cjs/structures/Player.js +56 -11
- package/dist/cjs/structures/Queue.d.ts +1 -1
- package/dist/cjs/structures/Utils.d.ts +56 -2
- package/dist/cjs/structures/Utils.js +32 -1
- package/dist/esm/structures/CustomSearches/BandCampSearch.js +1 -0
- package/dist/esm/structures/LavalinkManager.d.ts +51 -12
- package/dist/esm/structures/LavalinkManager.js +21 -14
- package/dist/esm/structures/Node.d.ts +11 -2
- package/dist/esm/structures/Node.js +110 -31
- package/dist/esm/structures/Player.d.ts +15 -2
- package/dist/esm/structures/Player.js +56 -11
- package/dist/esm/structures/Queue.d.ts +1 -1
- package/dist/esm/structures/Utils.d.ts +56 -2
- package/dist/esm/structures/Utils.js +30 -0
- package/dist/types/structures/LavalinkManager.d.ts +51 -12
- package/dist/types/structures/Node.d.ts +11 -2
- package/dist/types/structures/Player.d.ts +15 -2
- package/dist/types/structures/Queue.d.ts +1 -1
- package/dist/types/structures/Utils.d.ts +56 -2
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { Pool } from "undici";
|
|
|
3
3
|
import WebSocket from "ws";
|
|
4
4
|
import { DestroyReasons } from "./Player";
|
|
5
5
|
import { NodeSymbol, queueTrackEnd } from "./Utils";
|
|
6
|
+
export const validSponsorBlocks = ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler"];
|
|
6
7
|
export class LavalinkNode {
|
|
7
8
|
/** The provided Options of the Node */
|
|
8
9
|
options;
|
|
@@ -94,9 +95,8 @@ export class LavalinkNode {
|
|
|
94
95
|
}
|
|
95
96
|
async search(query, requestUser) {
|
|
96
97
|
const Query = this.NodeManager.LavalinkManager.utils.transformQuery(query);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
else if (Query.source)
|
|
98
|
+
this.NodeManager.LavalinkManager.utils.validateQueryString(this, Query.query);
|
|
99
|
+
if (Query.source)
|
|
100
100
|
this.NodeManager.LavalinkManager.utils.validateSourceString(this, Query.source);
|
|
101
101
|
if (["bcsearch", "bandcamp"].includes(Query.source)) {
|
|
102
102
|
throw new Error("Bandcamp Search only works on the player!");
|
|
@@ -562,6 +562,7 @@ export class LavalinkNode {
|
|
|
562
562
|
return;
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
|
+
// LAVALINK EVENT HANDLING UTIL FUNCTION
|
|
565
566
|
async handleEvent(payload) {
|
|
566
567
|
if (!payload.guildId)
|
|
567
568
|
return;
|
|
@@ -584,20 +585,36 @@ export class LavalinkNode {
|
|
|
584
585
|
case "WebSocketClosedEvent":
|
|
585
586
|
this.socketClosed(player, payload);
|
|
586
587
|
break;
|
|
588
|
+
case "SegmentsLoaded":
|
|
589
|
+
this.SponsorBlockSegmentLoaded(player, player.queue.current, payload);
|
|
590
|
+
break;
|
|
591
|
+
case "SegmentSkipped":
|
|
592
|
+
this.SponsorBlockSegmentkipped(player, player.queue.current, payload);
|
|
593
|
+
break;
|
|
594
|
+
case "ChaptersLoaded":
|
|
595
|
+
this.SponsorBlockChaptersLoaded(player, player.queue.current, payload);
|
|
596
|
+
break;
|
|
597
|
+
case "ChapterStarted":
|
|
598
|
+
this.SponsorBlockChapterStarted(player, player.queue.current, payload);
|
|
599
|
+
break;
|
|
587
600
|
default:
|
|
588
601
|
this.NodeManager.emit("error", this, new Error(`Node#event unknown event '${payload.type}'.`), payload);
|
|
589
602
|
break;
|
|
590
603
|
}
|
|
591
604
|
return;
|
|
592
605
|
}
|
|
606
|
+
// LAVALINK EVENT HANDLING FUNCTIONS
|
|
593
607
|
trackStart(player, track, payload) {
|
|
594
608
|
player.playing = true;
|
|
595
609
|
player.paused = false;
|
|
610
|
+
// don't emit the event if previous track == new track aka track loop
|
|
611
|
+
if (this.NodeManager.LavalinkManager.options?.emitNewSongsOnly === true && player.queue.previous[0]?.info?.identifier === track?.info?.identifier)
|
|
612
|
+
return;
|
|
596
613
|
return this.NodeManager.LavalinkManager.emit("trackStart", player, track, payload);
|
|
597
614
|
}
|
|
598
615
|
async trackEnd(player, track, payload) {
|
|
599
616
|
// If there are no songs in the queue
|
|
600
|
-
if (!player.queue.tracks.length && player.repeatMode === "off")
|
|
617
|
+
if (!player.queue.tracks.length && (player.repeatMode === "off" || player.get("internal_stopPlaying")))
|
|
601
618
|
return this.queueEnd(player, track, payload);
|
|
602
619
|
// If a track was forcibly played
|
|
603
620
|
if (payload.reason === "replaced")
|
|
@@ -616,6 +633,12 @@ export class LavalinkNode {
|
|
|
616
633
|
// remove tracks from the queue
|
|
617
634
|
if (player.repeatMode !== "track")
|
|
618
635
|
await queueTrackEnd(player);
|
|
636
|
+
else if (player.queue.current) { // If there was a current Track already and repeatmode === true, add it to the queue.
|
|
637
|
+
player.queue.previous.unshift(player.queue.current);
|
|
638
|
+
if (player.queue.previous.length > player.queue.options.maxPreviousTracks)
|
|
639
|
+
player.queue.previous.splice(player.queue.options.maxPreviousTracks, player.queue.previous.length);
|
|
640
|
+
await player.queue.utils.save();
|
|
641
|
+
}
|
|
619
642
|
// if no track available, end queue
|
|
620
643
|
if (!player.queue.current)
|
|
621
644
|
return this.queueEnd(player, track, payload);
|
|
@@ -624,11 +647,92 @@ export class LavalinkNode {
|
|
|
624
647
|
// play track if autoSkip is true
|
|
625
648
|
return this.NodeManager.LavalinkManager.options.autoSkip && player.play({ noReplace: true });
|
|
626
649
|
}
|
|
650
|
+
async trackStuck(player, track, payload) {
|
|
651
|
+
this.NodeManager.LavalinkManager.emit("trackStuck", player, track, payload);
|
|
652
|
+
// If there are no songs in the queue
|
|
653
|
+
if (!player.queue.tracks.length && (player.repeatMode === "off" || player.get("internal_stopPlaying")))
|
|
654
|
+
return this.queueEnd(player, track, payload);
|
|
655
|
+
// remove the current track, and enqueue the next one
|
|
656
|
+
await queueTrackEnd(player);
|
|
657
|
+
// if no track available, end queue
|
|
658
|
+
if (!player.queue.current)
|
|
659
|
+
return this.queueEnd(player, track, payload);
|
|
660
|
+
// play track if autoSkip is true
|
|
661
|
+
return (this.NodeManager.LavalinkManager.options.autoSkip && player.queue.current) && player.play({ noReplace: true });
|
|
662
|
+
}
|
|
663
|
+
async trackError(player, track, payload) {
|
|
664
|
+
this.NodeManager.LavalinkManager.emit("trackError", player, track, payload);
|
|
665
|
+
// If there are no songs in the queue
|
|
666
|
+
if (!player.queue.tracks.length && (player.repeatMode === "off" || player.get("internal_stopPlaying")))
|
|
667
|
+
return this.queueEnd(player, track, payload);
|
|
668
|
+
// remove the current track, and enqueue the next one
|
|
669
|
+
await queueTrackEnd(player);
|
|
670
|
+
// if no track available, end queue
|
|
671
|
+
if (!player.queue.current)
|
|
672
|
+
return this.queueEnd(player, track, payload);
|
|
673
|
+
// play track if autoSkip is true
|
|
674
|
+
return (this.NodeManager.LavalinkManager.options.autoSkip && player.queue.current) && player.play({ noReplace: true });
|
|
675
|
+
}
|
|
676
|
+
socketClosed(player, payload) {
|
|
677
|
+
return this.NodeManager.LavalinkManager.emit("playerSocketClosed", player, payload);
|
|
678
|
+
}
|
|
679
|
+
// SPONSOR BLOCK EVENT FUNCTIONS
|
|
680
|
+
SponsorBlockSegmentLoaded(player, track, payload) {
|
|
681
|
+
return this.NodeManager.LavalinkManager.emit("SegmentsLoaded", player, track, payload);
|
|
682
|
+
}
|
|
683
|
+
SponsorBlockSegmentkipped(player, track, payload) {
|
|
684
|
+
return this.NodeManager.LavalinkManager.emit("SegmentSkipped", player, track, payload);
|
|
685
|
+
}
|
|
686
|
+
SponsorBlockChaptersLoaded(player, track, payload) {
|
|
687
|
+
return this.NodeManager.LavalinkManager.emit("ChaptersLoaded", player, track, payload);
|
|
688
|
+
}
|
|
689
|
+
SponsorBlockChapterStarted(player, track, payload) {
|
|
690
|
+
return this.NodeManager.LavalinkManager.emit("ChapterStarted", player, track, payload);
|
|
691
|
+
}
|
|
692
|
+
// SPONSOR BLOCK EXECUTE FUNCTIONS
|
|
693
|
+
async getSponsorBlock(player) {
|
|
694
|
+
// no plugin enabled
|
|
695
|
+
if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
|
|
696
|
+
throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
|
|
697
|
+
// do the request
|
|
698
|
+
return await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`);
|
|
699
|
+
}
|
|
700
|
+
async setSponsorBlock(player, segments = ["sponsor", "selfpromo"]) {
|
|
701
|
+
// no plugin enabled
|
|
702
|
+
if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
|
|
703
|
+
throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
|
|
704
|
+
// no segments length
|
|
705
|
+
if (!segments.length)
|
|
706
|
+
throw new RangeError("No Segments provided. Did you ment to use 'deleteSponsorBlock'?");
|
|
707
|
+
// a not valid segment
|
|
708
|
+
if (segments.some(v => !validSponsorBlocks.includes(v.toLowerCase())))
|
|
709
|
+
throw new SyntaxError(`You provided a sponsorblock which isn't valid, valid ones are: ${validSponsorBlocks.map(v => `'${v}'`).join(", ")}`);
|
|
710
|
+
// do the request
|
|
711
|
+
await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, (request) => {
|
|
712
|
+
request.method = "PUT";
|
|
713
|
+
request.body = JSON.stringify(segments.map(v => v.toLowerCase()));
|
|
714
|
+
return request;
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
async deleteSponsorBlock(player) {
|
|
719
|
+
// no plugin enabled
|
|
720
|
+
if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
|
|
721
|
+
throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
|
|
722
|
+
// do the request
|
|
723
|
+
await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, (request) => {
|
|
724
|
+
request.method = "DELETE";
|
|
725
|
+
return request;
|
|
726
|
+
});
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
// UTIL FOR QUEUE END
|
|
627
730
|
async queueEnd(player, track, payload) {
|
|
628
731
|
// add previous track to the queue!
|
|
629
732
|
player.queue.current = null;
|
|
630
733
|
player.playing = false;
|
|
631
|
-
|
|
734
|
+
player.set("internal_stopPlaying", undefined);
|
|
735
|
+
if (typeof this.NodeManager.LavalinkManager.options?.playerOptions?.onEmptyQueue?.autoPlayFunction === "function" && typeof player.get("internal_autoplayStopPlaying") === "undefined") {
|
|
632
736
|
await this.NodeManager.LavalinkManager.options?.playerOptions?.onEmptyQueue?.autoPlayFunction(player, track);
|
|
633
737
|
if (player.queue.tracks.length > 0)
|
|
634
738
|
await queueTrackEnd(player);
|
|
@@ -638,6 +742,7 @@ export class LavalinkNode {
|
|
|
638
742
|
return player.play({ noReplace: true, paused: false });
|
|
639
743
|
}
|
|
640
744
|
}
|
|
745
|
+
player.set("internal_autoplayStopPlaying", undefined);
|
|
641
746
|
player.queue.previous.unshift(track);
|
|
642
747
|
if (payload?.reason !== "stopped") {
|
|
643
748
|
await player.queue.utils.save();
|
|
@@ -657,30 +762,4 @@ export class LavalinkNode {
|
|
|
657
762
|
}
|
|
658
763
|
return this.NodeManager.LavalinkManager.emit("queueEnd", player, track, payload);
|
|
659
764
|
}
|
|
660
|
-
async trackStuck(player, track, payload) {
|
|
661
|
-
this.NodeManager.LavalinkManager.emit("trackStuck", player, track, payload);
|
|
662
|
-
// If there are no songs in the queue
|
|
663
|
-
if (!player.queue.tracks.length && player.repeatMode === "off")
|
|
664
|
-
return;
|
|
665
|
-
// remove the current track, and enqueue the next one
|
|
666
|
-
await queueTrackEnd(player);
|
|
667
|
-
// if no track available, end queue
|
|
668
|
-
if (!player.queue.current)
|
|
669
|
-
return this.queueEnd(player, track, payload);
|
|
670
|
-
// play track if autoSkip is true
|
|
671
|
-
return (this.NodeManager.LavalinkManager.options.autoSkip && player.queue.current) && player.play({ noReplace: true });
|
|
672
|
-
}
|
|
673
|
-
async trackError(player, track, payload) {
|
|
674
|
-
this.NodeManager.LavalinkManager.emit("trackError", player, track, payload);
|
|
675
|
-
// remove the current track, and enqueue the next one
|
|
676
|
-
await queueTrackEnd(player);
|
|
677
|
-
// if no track available, end queue
|
|
678
|
-
if (!player.queue.current)
|
|
679
|
-
return this.queueEnd(player, track, payload);
|
|
680
|
-
// play track if autoSkip is true
|
|
681
|
-
return (this.NodeManager.LavalinkManager.options.autoSkip && player.queue.current) && player.play({ noReplace: true });
|
|
682
|
-
}
|
|
683
|
-
socketClosed(player, payload) {
|
|
684
|
-
return this.NodeManager.LavalinkManager.emit("playerSocketClosed", player, payload);
|
|
685
|
-
}
|
|
686
765
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EQBand, FilterData, FilterManager, LavalinkFilterData } from "./Filters";
|
|
2
2
|
import { LavalinkManager } from "./LavalinkManager";
|
|
3
|
-
import { LavalinkNode } from "./Node";
|
|
3
|
+
import { LavalinkNode, SponsorBlockSegment } from "./Node";
|
|
4
4
|
import { Queue } from "./Queue";
|
|
5
5
|
import { Track, UnresolvedTrack } from "./Track";
|
|
6
6
|
import { LavalinkPlayerVoiceOptions, LavaSearchQuery, SearchQuery } from "./Utils";
|
|
@@ -149,6 +149,9 @@ export declare class Player {
|
|
|
149
149
|
*/
|
|
150
150
|
setVolume(volume: number, ignoreVolumeDecrementer?: boolean): Promise<this>;
|
|
151
151
|
lavaSearch(query: LavaSearchQuery, requestUser: unknown): Promise<import("./Utils").SearchResult | import("./Utils").LavaSearchResponse>;
|
|
152
|
+
setSponsorBlock(segments?: SponsorBlockSegment[]): Promise<void>;
|
|
153
|
+
getSponsorBlock(): Promise<SponsorBlockSegment[]>;
|
|
154
|
+
deleteSponsorBlock(): Promise<void>;
|
|
152
155
|
/**
|
|
153
156
|
*
|
|
154
157
|
* @param query Query for your data
|
|
@@ -177,12 +180,22 @@ export declare class Player {
|
|
|
177
180
|
* Skip the current song, or a specific amount of songs
|
|
178
181
|
* @param amount provide the index of the next track to skip to
|
|
179
182
|
*/
|
|
180
|
-
skip(skipTo?: number): Promise<any>;
|
|
183
|
+
skip(skipTo?: number, throwError?: boolean): Promise<any>;
|
|
184
|
+
/**
|
|
185
|
+
* Clears the queue and stops playing. Does not destroy the Player and not leave the channel
|
|
186
|
+
* @returns
|
|
187
|
+
*/
|
|
188
|
+
stopPlaying(clearQueue?: boolean, executeAutoplay?: boolean): Promise<this>;
|
|
181
189
|
/**
|
|
182
190
|
* Connects the Player to the Voice Channel
|
|
183
191
|
* @returns
|
|
184
192
|
*/
|
|
185
193
|
connect(): Promise<this>;
|
|
194
|
+
changeVoiceState(data: {
|
|
195
|
+
voiceChannelId?: string;
|
|
196
|
+
selfDeaf?: boolean;
|
|
197
|
+
selfMute?: boolean;
|
|
198
|
+
}): Promise<this>;
|
|
186
199
|
/**
|
|
187
200
|
* Disconnects the Player from the Voice Channel, but keeps the player in the cache
|
|
188
201
|
* @param force If false it throws an error, if player thinks it's already disconnected
|
|
@@ -212,6 +212,15 @@ export class Player {
|
|
|
212
212
|
async lavaSearch(query, requestUser) {
|
|
213
213
|
return this.node.lavaSearch(query, requestUser);
|
|
214
214
|
}
|
|
215
|
+
async setSponsorBlock(segments = ["sponsor", "selfpromo"]) {
|
|
216
|
+
return this.node.setSponsorBlock(this, segments);
|
|
217
|
+
}
|
|
218
|
+
async getSponsorBlock() {
|
|
219
|
+
return this.node.getSponsorBlock(this);
|
|
220
|
+
}
|
|
221
|
+
async deleteSponsorBlock() {
|
|
222
|
+
return this.node.deleteSponsorBlock(this);
|
|
223
|
+
}
|
|
215
224
|
/**
|
|
216
225
|
*
|
|
217
226
|
* @param query Query for your data
|
|
@@ -219,10 +228,6 @@ export class Player {
|
|
|
219
228
|
*/
|
|
220
229
|
async search(query, requestUser) {
|
|
221
230
|
const Query = this.LavalinkManager.utils.transformQuery(query);
|
|
222
|
-
if (/^https?:\/\//.test(Query.query))
|
|
223
|
-
this.LavalinkManager.utils.validateQueryString(this.node, Query.source);
|
|
224
|
-
else if (Query.source)
|
|
225
|
-
this.LavalinkManager.utils.validateSourceString(this.node, Query.source);
|
|
226
231
|
if (["bcsearch", "bandcamp"].includes(Query.source))
|
|
227
232
|
return await bandCampSearch(this, Query.query, requestUser);
|
|
228
233
|
return this.node.search(Query, requestUser);
|
|
@@ -286,8 +291,8 @@ export class Player {
|
|
|
286
291
|
* Skip the current song, or a specific amount of songs
|
|
287
292
|
* @param amount provide the index of the next track to skip to
|
|
288
293
|
*/
|
|
289
|
-
async skip(skipTo = 0) {
|
|
290
|
-
if (!this.queue.tracks.length)
|
|
294
|
+
async skip(skipTo = 0, throwError = true) {
|
|
295
|
+
if (!this.queue.tracks.length && (throwError || (typeof skipTo === "boolean" && skipTo === true)))
|
|
291
296
|
throw new RangeError("Can't skip more than the queue size");
|
|
292
297
|
if (typeof skipTo === "number" && skipTo > 1) {
|
|
293
298
|
if (skipTo > this.queue.tracks.length)
|
|
@@ -301,13 +306,33 @@ export class Player {
|
|
|
301
306
|
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
|
|
302
307
|
return this;
|
|
303
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Clears the queue and stops playing. Does not destroy the Player and not leave the channel
|
|
311
|
+
* @returns
|
|
312
|
+
*/
|
|
313
|
+
async stopPlaying(clearQueue = true, executeAutoplay = false) {
|
|
314
|
+
// use internal_stopPlaying on true, so that it doesn't utilize current loop states. on trackEnd event
|
|
315
|
+
this.set("internal_stopPlaying", true);
|
|
316
|
+
// remove tracks from the queue
|
|
317
|
+
if (this.queue.tracks.length && clearQueue === true)
|
|
318
|
+
await this.queue.splice(0, this.queue.tracks.length);
|
|
319
|
+
if (executeAutoplay === false)
|
|
320
|
+
this.set("internal_autoplayStopPlaying", true);
|
|
321
|
+
else
|
|
322
|
+
this.set("internal_autoplayStopPlaying", undefined);
|
|
323
|
+
const now = performance.now();
|
|
324
|
+
// send to lavalink, that it should stop playing
|
|
325
|
+
await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { encodedTrack: null } });
|
|
326
|
+
this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
|
|
327
|
+
return this;
|
|
328
|
+
}
|
|
304
329
|
/**
|
|
305
330
|
* Connects the Player to the Voice Channel
|
|
306
331
|
* @returns
|
|
307
332
|
*/
|
|
308
333
|
async connect() {
|
|
309
334
|
if (!this.options.voiceChannelId)
|
|
310
|
-
throw new RangeError("No Voice Channel id has been set.");
|
|
335
|
+
throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
|
|
311
336
|
await this.LavalinkManager.options.sendToShard(this.guildId, {
|
|
312
337
|
op: 4,
|
|
313
338
|
d: {
|
|
@@ -317,6 +342,26 @@ export class Player {
|
|
|
317
342
|
self_deaf: this.options.selfDeaf ?? true,
|
|
318
343
|
}
|
|
319
344
|
});
|
|
345
|
+
this.voiceChannelId = this.options.voiceChannelId;
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
async changeVoiceState(data) {
|
|
349
|
+
if (this.options.voiceChannelId === data.voiceChannelId)
|
|
350
|
+
throw new RangeError("New Channel can't be equal to the old Channel.");
|
|
351
|
+
await this.LavalinkManager.options.sendToShard(this.guildId, {
|
|
352
|
+
op: 4,
|
|
353
|
+
d: {
|
|
354
|
+
guild_id: this.guildId,
|
|
355
|
+
channel_id: data.voiceChannelId,
|
|
356
|
+
self_mute: data.selfMute ?? this.options.selfMute ?? false,
|
|
357
|
+
self_deaf: data.selfDeaf ?? this.options.selfDeaf ?? true,
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
// override the options
|
|
361
|
+
this.options.voiceChannelId = data.voiceChannelId;
|
|
362
|
+
this.options.selfMute = data.selfMute;
|
|
363
|
+
this.options.selfDeaf = data.selfDeaf;
|
|
364
|
+
this.voiceChannelId = data.voiceChannelId;
|
|
320
365
|
return this;
|
|
321
366
|
}
|
|
322
367
|
/**
|
|
@@ -326,7 +371,7 @@ export class Player {
|
|
|
326
371
|
*/
|
|
327
372
|
async disconnect(force = false) {
|
|
328
373
|
if (!force && !this.options.voiceChannelId)
|
|
329
|
-
throw new RangeError("No Voice Channel id has been set.");
|
|
374
|
+
throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
|
|
330
375
|
await this.LavalinkManager.options.sendToShard(this.guildId, {
|
|
331
376
|
op: 4,
|
|
332
377
|
d: {
|
|
@@ -343,10 +388,10 @@ export class Player {
|
|
|
343
388
|
* Destroy the player and disconnect from the voice channel
|
|
344
389
|
*/
|
|
345
390
|
async destroy(reason, disconnect = true) {
|
|
346
|
-
if (this.LavalinkManager.options.debugOptions.playerDestroy.debugLog)
|
|
391
|
+
if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
|
|
347
392
|
console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Destroy-Reason: ${String(reason)}`);
|
|
348
393
|
if (this.get("internal_destroystatus") === true) {
|
|
349
|
-
if (this.LavalinkManager.options.debugOptions.playerDestroy.debugLog)
|
|
394
|
+
if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
|
|
350
395
|
console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Already destroying somewhere else..`);
|
|
351
396
|
return;
|
|
352
397
|
}
|
|
@@ -362,7 +407,7 @@ export class Player {
|
|
|
362
407
|
this.LavalinkManager.deletePlayer(this.guildId);
|
|
363
408
|
// destroy the player on lavalink side
|
|
364
409
|
await this.node.destroyPlayer(this.guildId);
|
|
365
|
-
if (this.LavalinkManager.options.debugOptions.playerDestroy.debugLog)
|
|
410
|
+
if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
|
|
366
411
|
console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Player got destroyed successfully`);
|
|
367
412
|
// emit the event
|
|
368
413
|
this.LavalinkManager.emit("playerDestroy", this, reason);
|
|
@@ -18,7 +18,7 @@ export interface QueueStoreManager extends Record<string, any> {
|
|
|
18
18
|
parse: (value: unknown) => Promise<Partial<StoredQueue>>;
|
|
19
19
|
}
|
|
20
20
|
export interface ManagerQueueOptions {
|
|
21
|
-
/** Maximum Amount of tracks for the queue.previous array */
|
|
21
|
+
/** Maximum Amount of tracks for the queue.previous array. Set to 0 to not save previous songs. Defaults to 25 Tracks */
|
|
22
22
|
maxPreviousTracks?: number;
|
|
23
23
|
/** Custom Queue Store option */
|
|
24
24
|
queueStore?: QueueStoreManager;
|
|
@@ -51,6 +51,17 @@ export interface UnresolvedSearchResult {
|
|
|
51
51
|
playlist: PlaylistInfo | null;
|
|
52
52
|
tracks: UnresolvedTrack[];
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Parses Node Connection Url: "lavalink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>"
|
|
56
|
+
* @param connectionUrl
|
|
57
|
+
* @returns
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseLavalinkConnUrl(connectionUrl: string): {
|
|
60
|
+
authorization: string;
|
|
61
|
+
id: string;
|
|
62
|
+
host: string;
|
|
63
|
+
port: number;
|
|
64
|
+
};
|
|
54
65
|
export declare class ManagerUtils {
|
|
55
66
|
LavalinkManager: LavalinkManager | null;
|
|
56
67
|
constructor(LavalinkManager?: LavalinkManager);
|
|
@@ -152,7 +163,7 @@ export declare class MiniMap<K, V> extends Map<K, V> {
|
|
|
152
163
|
map<T>(fn: (value: V, key: K, miniMap: this) => T): T[];
|
|
153
164
|
map<This, T>(fn: (this: This, value: V, key: K, miniMap: this) => T, thisArg: This): T[];
|
|
154
165
|
}
|
|
155
|
-
export type PlayerEvents = TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent;
|
|
166
|
+
export type PlayerEvents = TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent | SponsorBlockSegmentEvents;
|
|
156
167
|
export type Severity = "COMMON" | "SUSPICIOUS" | "FAULT";
|
|
157
168
|
export interface Exception {
|
|
158
169
|
severity: Severity;
|
|
@@ -188,9 +199,52 @@ export interface WebSocketClosedEvent extends PlayerEvent {
|
|
|
188
199
|
byRemote: boolean;
|
|
189
200
|
reason: string;
|
|
190
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Types & Events for Sponsorblock-plugin from Lavalink: https://github.com/topi314/Sponsorblock-Plugin#segmentsloaded
|
|
204
|
+
*/
|
|
205
|
+
export type SponsorBlockSegmentEvents = SponsorBlockSegmentSkipped | SponsorBlockSegmentsLoaded | SponsorBlockChapterStarted | SponsorBlockChaptersLoaded;
|
|
206
|
+
export type SponsorBlockSegmentEventType = "SegmentSkipped" | "SegmentsLoaded" | "ChaptersLoaded" | "ChapterStarted";
|
|
207
|
+
export interface SponsorBlockSegmentsLoaded extends PlayerEvent {
|
|
208
|
+
type: "SegmentsLoaded";
|
|
209
|
+
segments: {
|
|
210
|
+
category: string;
|
|
211
|
+
start: number;
|
|
212
|
+
end: number;
|
|
213
|
+
}[];
|
|
214
|
+
}
|
|
215
|
+
export interface SponsorBlockSegmentSkipped extends PlayerEvent {
|
|
216
|
+
type: "SegmentSkipped";
|
|
217
|
+
segment: {
|
|
218
|
+
category: string;
|
|
219
|
+
start: number;
|
|
220
|
+
end: number;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export interface SponsorBlockChapterStarted extends PlayerEvent {
|
|
224
|
+
type: "ChapterStarted";
|
|
225
|
+
/** The Chapter which started */
|
|
226
|
+
chapter: {
|
|
227
|
+
/** The Name of the Chapter */
|
|
228
|
+
name: string;
|
|
229
|
+
start: number;
|
|
230
|
+
end: number;
|
|
231
|
+
duration: number;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export interface SponsorBlockChaptersLoaded extends PlayerEvent {
|
|
235
|
+
type: "ChaptersLoaded";
|
|
236
|
+
/** All Chapters loaded */
|
|
237
|
+
chapters: {
|
|
238
|
+
/** The Name of the Chapter */
|
|
239
|
+
name: string;
|
|
240
|
+
start: number;
|
|
241
|
+
end: number;
|
|
242
|
+
duration: number;
|
|
243
|
+
}[];
|
|
244
|
+
}
|
|
191
245
|
export type LoadTypes = "track" | "playlist" | "search" | "error" | "empty";
|
|
192
246
|
export type State = "CONNECTED" | "CONNECTING" | "DISCONNECTED" | "DISCONNECTING" | "DESTROYING";
|
|
193
|
-
export type PlayerEventType = "TrackStartEvent" | "TrackEndEvent" | "TrackExceptionEvent" | "TrackStuckEvent" | "WebSocketClosedEvent";
|
|
247
|
+
export type PlayerEventType = "TrackStartEvent" | "TrackEndEvent" | "TrackExceptionEvent" | "TrackStuckEvent" | "WebSocketClosedEvent" | SponsorBlockSegmentEventType;
|
|
194
248
|
export type TrackEndReason = "finished" | "loadFailed" | "stopped" | "replaced" | "cleanup";
|
|
195
249
|
export interface InvalidLavalinkRestRequest {
|
|
196
250
|
timestamp: number;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { URL } from "node:url";
|
|
2
|
+
import { isRegExp } from "node:util/types";
|
|
1
3
|
import { DefaultSources, LavalinkPlugins, SourceLinksRegexes } from "./LavalinkManagerStatics";
|
|
2
4
|
export const TrackSymbol = Symbol("LC-Track");
|
|
3
5
|
export const UnresolvedTrackSymbol = Symbol("LC-Track-Unresolved");
|
|
@@ -5,6 +7,22 @@ export const QueueSymbol = Symbol("LC-Queue");
|
|
|
5
7
|
export const NodeSymbol = Symbol("LC-Node");
|
|
6
8
|
/** @hidden */
|
|
7
9
|
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10
|
+
/**
|
|
11
|
+
* Parses Node Connection Url: "lavalink://<nodeId>:<nodeAuthorization(Password)>@<NodeHost>:<NodePort>"
|
|
12
|
+
* @param connectionUrl
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export function parseLavalinkConnUrl(connectionUrl) {
|
|
16
|
+
if (!connectionUrl.startsWith("lavalink://"))
|
|
17
|
+
throw new Error(`ConnectionUrl (${connectionUrl}) must start with 'lavalink://'`);
|
|
18
|
+
const parsed = new URL(connectionUrl);
|
|
19
|
+
return {
|
|
20
|
+
authorization: parsed.password,
|
|
21
|
+
id: parsed.username,
|
|
22
|
+
host: parsed.hostname,
|
|
23
|
+
port: Number(parsed.port),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
8
26
|
export class ManagerUtils {
|
|
9
27
|
LavalinkManager = null;
|
|
10
28
|
constructor(LavalinkManager) {
|
|
@@ -163,6 +181,18 @@ export class ManagerUtils {
|
|
|
163
181
|
throw new Error("No Lavalink Node was provided");
|
|
164
182
|
if (!node.info.sourceManagers?.length)
|
|
165
183
|
throw new Error("Lavalink Node, has no sourceManagers enabled");
|
|
184
|
+
// checks for blacklisted links / domains / queries
|
|
185
|
+
if (this.LavalinkManager.options?.linksBlacklist?.length > 0 && this.LavalinkManager.options?.linksBlacklist.some(v => (typeof v === "string" && (queryString.toLowerCase().includes(v.toLowerCase()) || v.toLowerCase().includes(queryString.toLowerCase()))) || isRegExp(v) && v.test(queryString))) {
|
|
186
|
+
throw new Error(`Query string contains a link / word which is blacklisted.`);
|
|
187
|
+
}
|
|
188
|
+
if (!/^https?:\/\//.test(queryString))
|
|
189
|
+
return;
|
|
190
|
+
else if (this.LavalinkManager.options?.linksAllowed === false)
|
|
191
|
+
throw new Error("Using links to make a request is not allowed.");
|
|
192
|
+
// checks for if the query is whitelisted (should only work for links, so it skips the check for no link queries)
|
|
193
|
+
if (this.LavalinkManager.options?.linksWhitelist?.length > 0 && !this.LavalinkManager.options?.linksWhitelist.some(v => (typeof v === "string" && (queryString.toLowerCase().includes(v.toLowerCase()) || v.toLowerCase().includes(queryString.toLowerCase()))) || isRegExp(v) && v.test(queryString))) {
|
|
194
|
+
throw new Error(`Query string contains a link / word which isn't whitelisted.`);
|
|
195
|
+
}
|
|
166
196
|
// missing links: beam.pro local getyarn.io clypit pornhub reddit ocreamix soundgasm
|
|
167
197
|
if ((SourceLinksRegexes.YoutubeMusicRegex.test(queryString) || SourceLinksRegexes.YoutubeRegex.test(queryString)) && !node.info?.sourceManagers?.includes("youtube")) {
|
|
168
198
|
throw new Error("Lavalink Node has not 'youtube' enabled");
|
|
@@ -5,7 +5,7 @@ import { NodeManager } from "./NodeManager";
|
|
|
5
5
|
import { DestroyReasonsType, Player, PlayerJson, PlayerOptions } from "./Player";
|
|
6
6
|
import { ManagerQueueOptions } from "./Queue";
|
|
7
7
|
import { Track, UnresolvedTrack } from "./Track";
|
|
8
|
-
import { ChannelDeletePacket, GuildShardPayload, ManagerUtils, MiniMap, SearchPlatform, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent, VoicePacket, VoiceServer, VoiceState, WebSocketClosedEvent } from "./Utils";
|
|
8
|
+
import { ChannelDeletePacket, GuildShardPayload, ManagerUtils, MiniMap, SearchPlatform, SponsorBlockChaptersLoaded, SponsorBlockChapterStarted, SponsorBlockSegmentSkipped, SponsorBlockSegmentsLoaded, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent, VoicePacket, VoiceServer, VoiceState, WebSocketClosedEvent } from "./Utils";
|
|
9
9
|
export interface LavalinkManager {
|
|
10
10
|
nodeManager: NodeManager;
|
|
11
11
|
utils: ManagerUtils;
|
|
@@ -33,7 +33,7 @@ export interface ManagerPlayerOptions {
|
|
|
33
33
|
onDisconnect?: {
|
|
34
34
|
/** Try to reconnect? -> If fails -> Destroy */
|
|
35
35
|
autoReconnect?: boolean;
|
|
36
|
-
/** Instantly destroy player (overrides autoReconnect) */
|
|
36
|
+
/** Instantly destroy player (overrides autoReconnect) | Don't provide == disable feature*/
|
|
37
37
|
destroyPlayer?: boolean;
|
|
38
38
|
};
|
|
39
39
|
onEmptyQueue?: {
|
|
@@ -56,16 +56,27 @@ export interface ManagerOptions {
|
|
|
56
56
|
playerOptions?: ManagerPlayerOptions;
|
|
57
57
|
/** If it should skip to the next Track on TrackEnd / TrackError etc. events */
|
|
58
58
|
autoSkip?: boolean;
|
|
59
|
-
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
/** If it should emit only new (unique) songs and not when a looping track (or similar) is plaid, default false */
|
|
60
|
+
emitNewSongsOnly?: boolean;
|
|
61
|
+
/** Only allow link requests with links either matching some of that regExp or including some of that string */
|
|
62
|
+
linksWhitelist?: (RegExp | string)[];
|
|
63
|
+
/** Never allow link requests with links either matching some of that regExp or including some of that string (doesn't even allow if it's whitelisted) */
|
|
64
|
+
linksBlacklist?: (RegExp | string)[];
|
|
65
|
+
/** If links should be allowed or not. If set to false, it will throw an error if a link was provided. */
|
|
66
|
+
linksAllowed?: boolean;
|
|
67
|
+
/** Advanced Options for the Library, which may or may not be "library breaking" */
|
|
68
|
+
advancedOptions?: {
|
|
69
|
+
/** optional */
|
|
70
|
+
debugOptions?: {
|
|
71
|
+
/** logs for debugging the "no-Audio" playing error */
|
|
72
|
+
noAudio?: boolean;
|
|
73
|
+
/** For Logging the Destroy function */
|
|
74
|
+
playerDestroy?: {
|
|
75
|
+
/** To show the debug reason at all times. */
|
|
76
|
+
debugLog?: boolean;
|
|
77
|
+
/** If you get 'Error: Use Player#destroy("reason") not LavalinkManager#deletePlayer() to stop the Player' put it on true */
|
|
78
|
+
dontThrowError?: boolean;
|
|
79
|
+
};
|
|
69
80
|
};
|
|
70
81
|
};
|
|
71
82
|
}
|
|
@@ -125,6 +136,34 @@ interface LavalinkManagerEvents {
|
|
|
125
136
|
* @event Manager#playerUpdate
|
|
126
137
|
*/
|
|
127
138
|
"playerUpdate": (oldPlayerJson: PlayerJson, newPlayer: Player) => void;
|
|
139
|
+
/**
|
|
140
|
+
* SPONSORBLOCK-PLUGIN EVENT
|
|
141
|
+
* Emitted when Segments are loaded
|
|
142
|
+
* @link https://github.com/topi314/Sponsorblock-Plugin#segmentsloaded
|
|
143
|
+
* @event Manager#trackError
|
|
144
|
+
*/
|
|
145
|
+
"SegmentsLoaded": (player: Player, track: Track | UnresolvedTrack, payload: SponsorBlockSegmentsLoaded) => void;
|
|
146
|
+
/**
|
|
147
|
+
* SPONSORBLOCK-PLUGIN EVENT
|
|
148
|
+
* Emitted when a specific Segment was skipped
|
|
149
|
+
* @link https://github.com/topi314/Sponsorblock-Plugin#segmentskipped
|
|
150
|
+
* @event Manager#trackError
|
|
151
|
+
*/
|
|
152
|
+
"SegmentSkipped": (player: Player, track: Track | UnresolvedTrack, payload: SponsorBlockSegmentSkipped) => void;
|
|
153
|
+
/**
|
|
154
|
+
* SPONSORBLOCK-PLUGIN EVENT
|
|
155
|
+
* Emitted when a specific Chapter starts playing
|
|
156
|
+
* @link https://github.com/topi314/Sponsorblock-Plugin#chapterstarted
|
|
157
|
+
* @event Manager#trackError
|
|
158
|
+
*/
|
|
159
|
+
"ChapterStarted": (player: Player, track: Track | UnresolvedTrack, payload: SponsorBlockChapterStarted) => void;
|
|
160
|
+
/**
|
|
161
|
+
* SPONSORBLOCK-PLUGIN EVENT
|
|
162
|
+
* Emitted when Chapters are loaded
|
|
163
|
+
* @link https://github.com/topi314/Sponsorblock-Plugin#chaptersloaded
|
|
164
|
+
* @event Manager#trackError
|
|
165
|
+
*/
|
|
166
|
+
"ChaptersLoaded": (player: Player, track: Track | UnresolvedTrack, payload: SponsorBlockChaptersLoaded) => void;
|
|
128
167
|
}
|
|
129
168
|
export interface LavalinkManager {
|
|
130
169
|
options: ManagerOptions;
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import internal from "stream";
|
|
3
3
|
import { Dispatcher, Pool } from "undici";
|
|
4
4
|
import { NodeManager } from "./NodeManager";
|
|
5
|
-
import { DestroyReasonsType } from "./Player";
|
|
5
|
+
import { DestroyReasonsType, Player } from "./Player";
|
|
6
6
|
import { Track } from "./Track";
|
|
7
7
|
import { Base64, InvalidLavalinkRestRequest, LavalinkPlayer, LavaSearchQuery, LavaSearchResponse, PlayerUpdateInfo, RoutePlanner, SearchQuery, SearchResult, Session } from "./Utils";
|
|
8
8
|
/** Modifies any outgoing REST requests. */
|
|
9
9
|
export type ModifyRequest = (options: Dispatcher.RequestOptions) => void;
|
|
10
|
+
export declare const validSponsorBlocks: string[];
|
|
11
|
+
export type SponsorBlockSegment = "sponsor" | "selfpromo" | "interaction" | "intro" | "outro" | "preview" | "music_offtopic" | "filler";
|
|
10
12
|
export interface LavalinkNodeOptions {
|
|
11
13
|
/** The Lavalink Server-Ip / Domain-URL */
|
|
12
14
|
host: string;
|
|
@@ -240,8 +242,15 @@ export declare class LavalinkNode {
|
|
|
240
242
|
private handleEvent;
|
|
241
243
|
private trackStart;
|
|
242
244
|
private trackEnd;
|
|
243
|
-
private queueEnd;
|
|
244
245
|
private trackStuck;
|
|
245
246
|
private trackError;
|
|
246
247
|
private socketClosed;
|
|
248
|
+
private SponsorBlockSegmentLoaded;
|
|
249
|
+
private SponsorBlockSegmentkipped;
|
|
250
|
+
private SponsorBlockChaptersLoaded;
|
|
251
|
+
private SponsorBlockChapterStarted;
|
|
252
|
+
getSponsorBlock(player: Player): Promise<SponsorBlockSegment[]>;
|
|
253
|
+
setSponsorBlock(player: Player, segments?: SponsorBlockSegment[]): Promise<void>;
|
|
254
|
+
deleteSponsorBlock(player: Player): Promise<void>;
|
|
255
|
+
private queueEnd;
|
|
247
256
|
}
|