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.
@@ -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
- if (/^https?:\/\//.test(Query.query))
98
- this.NodeManager.LavalinkManager.utils.validateQueryString(this, Query.source);
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
- if (typeof this.NodeManager.LavalinkManager.options?.playerOptions?.onEmptyQueue?.autoPlayFunction === "function") {
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
- /** optional */
60
- debugOptions?: {
61
- /** logs for debugging the "no-Audio" playing error */
62
- noAudio?: boolean;
63
- /** For Logging the Destroy function */
64
- playerDestroy?: {
65
- /** To show the debug reason at all times. */
66
- debugLog?: boolean;
67
- /** If you get 'Error: Use Player#destroy("reason") not LavalinkManager#deletePlayer() to stop the Player' put it on true */
68
- dontThrowError?: boolean;
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
  }