lavalink-client 1.1.24 → 1.2.0

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 CHANGED
@@ -82,6 +82,10 @@ Check out the [Documentation](https://lc4.gitbook.io/lavalink-client) for **Exam
82
82
  - 🛡️ Lavalink Validations
83
83
  - It only let's you use the filters / plugins / sources, if Lavalink actually has it enabled
84
84
 
85
+ - 🛡️ Client Validations
86
+ - Allows you to whitelist links and even blacklist links / words / domain names, so that it doesn't allow requests you don't want!
87
+ - Checks almost all Lavalink Requests for out of bound errors, right before the request is made to prevent process breaking errors.
88
+
85
89
  - 🧑‍💻 Memory friendly and easy style
86
90
  - Only the required data is displayed, and the store-way & types match Lavalink#IMPLEMENTATION.md
87
91
 
@@ -91,4 +95,25 @@ Check out the [Documentation](https://lc4.gitbook.io/lavalink-client) for **Exam
91
95
  - Pauses / resumes the player if it get's muted / unmuted (server-wide) [soon]
92
96
  - ...
93
97
 
94
- - 😁 Much much more!
98
+ - 😁 Much much more!
99
+
100
+ # UpdateLog
101
+
102
+ ## **Version 1.2.0**
103
+ - Added `player.stopPlaying()`: When executed it **clears the Queue** and **stops playing**, **without destroying the Player**
104
+ - Adjusted `Player.skip()`
105
+ - Added `throwError` Property to: `player.skip(skipTo?:number = 0, throwError?:boolean = true)`.
106
+ - If throwError = false, and no more tracks are in the queue, it won't throw an error and "ignore it". same thing as stopPlaying.
107
+ - Added all Events and Methods from the [SponsorBlock Plugin](https://github.com/topi314/Sponsorblock-Plugin).
108
+ - It also validates if the plugin is in the bot, in order so that you can use the functions:
109
+ - `player.getSponsorBlock()` / `node.getSponsorBlock()`
110
+ - `player.setSponsorBlock(segments:SponsorBlockSegment[])` / `node.setSponsorBlock(segments:SponsorBlockSegment[])`
111
+ - `player.deleteSponsorBlock()` / `node.deleteSponsorBlock()`
112
+ - That Plugin adds following **Events** to the **Manager**: `"SegmentsLoaded"`, `"SegmentSkipped"`, `"ChapterStarted"`, `"ChaptersLoaded"`
113
+ - Example Bot show example in autoplayFunction how to "disable" / "enable" Autoplay with bot data variables.
114
+ - Added `ManagerOptions#onlyEmitNewTracks`. If set to true, it won't emit "trackStart" Event, when track.loop is active, or the new current track == the previous (current) track.
115
+ - Added `ManagerOptions#linksBlacklist` which allows user to specify an array of regExp / strings to match query strings (for links / words) and if a match happens it doesn't allow the request (blacklist)
116
+ - Added `ManagerOptions#linksWhitelist` which allows user to specify an array of regExp / strings to match query strings (for links only) and if a match does NOT HAPPEN it doesn't allow the request (whitelist)
117
+ - Added `ManagerOptions#linksAllowed` if set to false, it does not allow requests which are links
118
+ - Moved `ManaagerOptions#debugOptions` to `ManaagerOptions#advancedOptions.debugOptions`
119
+
@@ -5,6 +5,7 @@ const undici_1 = require("undici");
5
5
  const bandCampSearch = async (player, query, requestUser) => {
6
6
  let error = null;
7
7
  let tracks = [];
8
+ player.LavalinkManager.utils.validateQueryString(player.node, query);
8
9
  try {
9
10
  const data = await (0, undici_1.fetch)(`https://bandcamp.com/api/nusearch/2/autocomplete?q=${encodeURIComponent(query)}`, {
10
11
  headers: {
@@ -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(true) not PlayerManager#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
  }
@@ -102,7 +113,7 @@ interface LavalinkManagerEvents {
102
113
  "playerCreate": (player: Player) => void;
103
114
  /**
104
115
  * Emitted when a Player is moved within the channel.
105
- * @event Manager.playerManager#move
116
+ * @event Manager#playerMove
106
117
  */
107
118
  "playerMove": (player: Player, oldVoiceChannelId: string, newVoiceChannelId: string) => void;
108
119
  /**
@@ -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;
@@ -141,7 +180,8 @@ export declare class LavalinkManager extends EventEmitter {
141
180
  constructor(options: ManagerOptions);
142
181
  createPlayer(options: PlayerOptions): Player;
143
182
  getPlayer(guildId: string): Player;
144
- deletePlayer(guildId: string, throwError?: boolean): boolean;
183
+ destroyPlayer(guildId: string, destroyReason?: string): Promise<Player>;
184
+ deletePlayer(guildId: string): boolean;
145
185
  get useable(): boolean;
146
186
  /**
147
187
  * Initiates the Manager.
@@ -37,17 +37,22 @@ class LavalinkManager extends events_1.EventEmitter {
37
37
  requesterTransformer: options?.playerOptions?.requesterTransformer ?? null,
38
38
  useUnresolvedData: options?.playerOptions?.useUnresolvedData ?? false,
39
39
  },
40
+ linksWhitelist: options?.linksWhitelist ?? [],
41
+ linksBlacklist: options?.linksBlacklist ?? [],
40
42
  autoSkip: options?.autoSkip ?? true,
43
+ emitNewSongsOnly: options?.emitNewSongsOnly ?? false,
41
44
  queueOptions: {
42
45
  maxPreviousTracks: options?.queueOptions?.maxPreviousTracks ?? 25,
43
46
  queueChangesWatcher: options?.queueOptions?.queueChangesWatcher ?? null,
44
47
  queueStore: options?.queueOptions?.queueStore ?? new Queue_1.DefaultQueueStore(),
45
48
  },
46
- debugOptions: {
47
- noAudio: options?.debugOptions?.noAudio ?? false,
48
- playerDestroy: {
49
- dontThrowError: options?.debugOptions?.playerDestroy?.dontThrowError ?? false,
50
- debugLog: options?.debugOptions?.playerDestroy?.debugLog ?? false,
49
+ advancedOptions: {
50
+ debugOptions: {
51
+ noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
52
+ playerDestroy: {
53
+ dontThrowError: options?.advancedOptions?.debugOptions?.playerDestroy?.dontThrowError ?? false,
54
+ debugLog: options?.advancedOptions?.debugOptions?.playerDestroy?.debugLog ?? false,
55
+ }
51
56
  }
52
57
  }
53
58
  };
@@ -60,6 +65,8 @@ class LavalinkManager extends events_1.EventEmitter {
60
65
  // if(typeof options?.client !== "object" || typeof options?.client.id !== "string") throw new SyntaxError("ManagerOption.client = { id: string, username?:string } was not provided, which is required");
61
66
  if (options?.autoSkip && typeof options?.autoSkip !== "boolean")
62
67
  throw new SyntaxError("ManagerOption.autoSkip must be either false | true aka boolean");
68
+ if (options?.emitNewSongsOnly && typeof options?.emitNewSongsOnly !== "boolean")
69
+ throw new SyntaxError("ManagerOption.emitNewSongsOnly must be either false | true aka boolean");
63
70
  if (!options?.nodes || !Array.isArray(options?.nodes) || !options?.nodes.every(node => this.utils.isNodeOptions(node)))
64
71
  throw new SyntaxError("ManagerOption.nodes must be an Array of NodeOptions and is required of at least 1 Node");
65
72
  /* QUEUE STORE */
@@ -101,15 +108,22 @@ class LavalinkManager extends events_1.EventEmitter {
101
108
  getPlayer(guildId) {
102
109
  return this.players.get(guildId);
103
110
  }
104
- deletePlayer(guildId, throwError = true) {
111
+ destroyPlayer(guildId, destroyReason) {
112
+ const oldPlayer = this.getPlayer(guildId);
113
+ if (!oldPlayer)
114
+ return;
115
+ return oldPlayer.destroy(destroyReason);
116
+ }
117
+ deletePlayer(guildId) {
105
118
  const oldPlayer = this.getPlayer(guildId);
106
119
  if (!oldPlayer)
107
120
  return;
108
- if (oldPlayer.voiceChannelId === "string" && oldPlayer.connected) {
109
- if (throwError)
110
- throw new Error(`Use Player#destroy(true) not PlayerManager#deletePlayer() to stop the Player ${JSON.stringify(oldPlayer.toJSON?.())}`);
121
+ // oldPlayer.connected is operational. you could also do oldPlayer.voice?.token
122
+ if (oldPlayer.voiceChannelId === "string" && oldPlayer.connected && !oldPlayer.get("internal_destroywithoutdisconnect")) {
123
+ if (!this.options?.advancedOptions?.debugOptions?.playerDestroy?.dontThrowError)
124
+ throw new Error(`Use Player#destroy() not LavalinkManager#deletePlayer() to stop the Player ${JSON.stringify(oldPlayer.toJSON?.())}`);
111
125
  else
112
- console.error("Use Player#destroy(true) not PlayerManager#deletePlayer() to stop the Player", oldPlayer.toJSON?.());
126
+ console.error("Use Player#destroy() not LavalinkManager#deletePlayer() to stop the Player", oldPlayer.toJSON?.());
113
127
  }
114
128
  return this.players.delete(guildId);
115
129
  }
@@ -152,12 +166,12 @@ class LavalinkManager extends events_1.EventEmitter {
152
166
  */
153
167
  async sendRawData(data) {
154
168
  if (!this.initiated) {
155
- if (this.options?.debugOptions?.noAudio === true)
169
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
156
170
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, manager is not initated yet");
157
171
  return;
158
172
  }
159
173
  if (!("t" in data)) {
160
- if (this.options?.debugOptions?.noAudio === true)
174
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
161
175
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no 't' in payload-data of the raw event:", data);
162
176
  return;
163
177
  }
@@ -174,23 +188,23 @@ class LavalinkManager extends events_1.EventEmitter {
174
188
  if (["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t)) {
175
189
  const update = ("d" in data ? data.d : data);
176
190
  if (!update) {
177
- if (this.options?.debugOptions?.noAudio === true)
191
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
178
192
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no update data found in payload:", data);
179
193
  return;
180
194
  }
181
195
  if (!("token" in update) && !("session_id" in update)) {
182
- if (this.options?.debugOptions?.noAudio === true)
196
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
183
197
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no 'token' nor 'session_id' found in payload:", data);
184
198
  return;
185
199
  }
186
200
  const player = this.getPlayer(update.guild_id);
187
201
  if (!player) {
188
- if (this.options?.debugOptions?.noAudio === true)
202
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
189
203
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, No Lavalink Player found via key: 'guild_id' of update-data:", update);
190
204
  return;
191
205
  }
192
206
  if (player.get("internal_destroystatus") === true) {
193
- if (this.options?.debugOptions?.noAudio === true)
207
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
194
208
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Player is in a destroying state. can't signal the voice states");
195
209
  return;
196
210
  }
@@ -207,13 +221,13 @@ class LavalinkManager extends events_1.EventEmitter {
207
221
  }
208
222
  }
209
223
  });
210
- if (this.options?.debugOptions?.noAudio === true)
224
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
211
225
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Sent updatePlayer for voice token session", { voice: { token: update.token, endpoint: update.endpoint, sessionId: player.voice?.sessionId, } });
212
226
  return;
213
227
  }
214
228
  /* voice state update */
215
229
  if (update.user_id !== this.options?.client.id) {
216
- if (this.options?.debugOptions?.noAudio === true)
230
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
217
231
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, voice update user is not equal to provided client id of the manageroptions#client#id", "user:", update.user_id, "manager client id:", this.options?.client.id);
218
232
  return;
219
233
  }
@@ -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;
@@ -238,8 +240,15 @@ export declare class LavalinkNode {
238
240
  private error;
239
241
  private message;
240
242
  private handleEvent;
243
+ private SponsorBlockSegmentLoaded;
244
+ private SponsorBlockSegmentkipped;
245
+ private SponsorBlockChaptersLoaded;
246
+ private SponsorBlockChapterStarted;
241
247
  private trackStart;
242
248
  private trackEnd;
249
+ getSponsorBlock(player: Player): Promise<SponsorBlockSegment[]>;
250
+ setSponsorBlock(player: Player, segments?: SponsorBlockSegment[]): Promise<void>;
251
+ deleteSponsorBlock(player: Player): Promise<void>;
243
252
  private queueEnd;
244
253
  private trackStuck;
245
254
  private trackError;
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LavalinkNode = void 0;
3
+ exports.LavalinkNode = exports.validSponsorBlocks = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const path_1 = require("path");
6
6
  const undici_1 = require("undici");
7
7
  const ws_1 = tslib_1.__importDefault(require("ws"));
8
8
  const Player_1 = require("./Player");
9
9
  const Utils_1 = require("./Utils");
10
+ exports.validSponsorBlocks = ["sponsor", "selfpromo", "interaction", "intro", "outro", "preview", "music_offtopic", "filler"];
10
11
  class LavalinkNode {
11
12
  /** The provided Options of the Node */
12
13
  options;
@@ -87,7 +88,7 @@ class LavalinkNode {
87
88
  modify?.(options);
88
89
  const url = new URL(`${this.poolAddress}${options.path}`);
89
90
  url.searchParams.append("trace", "true");
90
- options.path = url.toString().replace(this.poolAddress, "");
91
+ options.path = url.pathname + url.search;
91
92
  const request = await this.rest.request(options);
92
93
  this.calls++;
93
94
  if (options.method === "DELETE")
@@ -98,9 +99,8 @@ class LavalinkNode {
98
99
  }
99
100
  async search(query, requestUser) {
100
101
  const Query = this.NodeManager.LavalinkManager.utils.transformQuery(query);
101
- if (/^https?:\/\//.test(Query.query))
102
- this.NodeManager.LavalinkManager.utils.validateQueryString(this, Query.source);
103
- else if (Query.source)
102
+ this.NodeManager.LavalinkManager.utils.validateQueryString(this, Query.query);
103
+ if (Query.source)
104
104
  this.NodeManager.LavalinkManager.utils.validateSourceString(this, Query.source);
105
105
  if (["bcsearch", "bandcamp"].includes(Query.source)) {
106
106
  throw new Error("Bandcamp Search only works on the player!");
@@ -171,7 +171,7 @@ class LavalinkNode {
171
171
  if (data.noReplace) {
172
172
  const url = new URL(`${this.poolAddress}${r.path}`);
173
173
  url.searchParams.append("noReplace", data.noReplace?.toString() || "false");
174
- r.path = url.toString().replace(this.poolAddress, "");
174
+ r.path = url.pathname + url.search;
175
175
  }
176
176
  });
177
177
  return this.syncPlayerData({}, res), res;
@@ -588,15 +588,42 @@ class LavalinkNode {
588
588
  case "WebSocketClosedEvent":
589
589
  this.socketClosed(player, payload);
590
590
  break;
591
+ case "SegmentsLoaded":
592
+ this.SponsorBlockSegmentLoaded(player, player.queue.current, payload);
593
+ break;
594
+ case "SegmentSkipped":
595
+ this.SponsorBlockSegmentkipped(player, player.queue.current, payload);
596
+ break;
597
+ case "ChaptersLoaded":
598
+ this.SponsorBlockChaptersLoaded(player, player.queue.current, payload);
599
+ break;
600
+ case "ChapterStarted":
601
+ this.SponsorBlockChapterStarted(player, player.queue.current, payload);
602
+ break;
591
603
  default:
592
604
  this.NodeManager.emit("error", this, new Error(`Node#event unknown event '${payload.type}'.`), payload);
593
605
  break;
594
606
  }
595
607
  return;
596
608
  }
609
+ SponsorBlockSegmentLoaded(player, track, payload) {
610
+ return this.NodeManager.LavalinkManager.emit("SegmentsLoaded", player, track, payload);
611
+ }
612
+ SponsorBlockSegmentkipped(player, track, payload) {
613
+ return this.NodeManager.LavalinkManager.emit("SegmentSkipped", player, track, payload);
614
+ }
615
+ SponsorBlockChaptersLoaded(player, track, payload) {
616
+ return this.NodeManager.LavalinkManager.emit("ChaptersLoaded", player, track, payload);
617
+ }
618
+ SponsorBlockChapterStarted(player, track, payload) {
619
+ return this.NodeManager.LavalinkManager.emit("ChapterStarted", player, track, payload);
620
+ }
597
621
  trackStart(player, track, payload) {
598
622
  player.playing = true;
599
623
  player.paused = false;
624
+ // don't emit the event if previous track == new track aka track loop
625
+ if (this.NodeManager.LavalinkManager.options?.emitNewSongsOnly === true && player.queue.previous[0]?.info?.identifier === track?.info?.identifier)
626
+ return;
600
627
  return this.NodeManager.LavalinkManager.emit("trackStart", player, track, payload);
601
628
  }
602
629
  async trackEnd(player, track, payload) {
@@ -620,6 +647,12 @@ class LavalinkNode {
620
647
  // remove tracks from the queue
621
648
  if (player.repeatMode !== "track")
622
649
  await (0, Utils_1.queueTrackEnd)(player);
650
+ else if (player.queue.current) { // If there was a current Track already and repeatmode === true, add it to the queue.
651
+ player.queue.previous.unshift(player.queue.current);
652
+ if (player.queue.previous.length > player.queue.options.maxPreviousTracks)
653
+ player.queue.previous.splice(player.queue.options.maxPreviousTracks, player.queue.previous.length);
654
+ await player.queue.utils.save();
655
+ }
623
656
  // if no track available, end queue
624
657
  if (!player.queue.current)
625
658
  return this.queueEnd(player, track, payload);
@@ -628,6 +661,42 @@ class LavalinkNode {
628
661
  // play track if autoSkip is true
629
662
  return this.NodeManager.LavalinkManager.options.autoSkip && player.play({ noReplace: true });
630
663
  }
664
+ async getSponsorBlock(player) {
665
+ // no plugin enabled
666
+ if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
667
+ throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
668
+ // do the request
669
+ return await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`);
670
+ }
671
+ async setSponsorBlock(player, segments = ["sponsor", "selfpromo"]) {
672
+ // no plugin enabled
673
+ if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
674
+ throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
675
+ // no segments length
676
+ if (!segments.length)
677
+ throw new RangeError("No Segments provided. Did you ment to use 'deleteSponsorBlock'?");
678
+ // a not valid segment
679
+ if (segments.some(v => !exports.validSponsorBlocks.includes(v.toLowerCase())))
680
+ throw new SyntaxError(`You provided a sponsorblock which isn't valid, valid ones are: ${exports.validSponsorBlocks.map(v => `'${v}'`).join(", ")}`);
681
+ // do the request
682
+ await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, (request) => {
683
+ request.method = "PUT";
684
+ request.body = JSON.stringify(segments.map(v => v.toLowerCase()));
685
+ return request;
686
+ });
687
+ return;
688
+ }
689
+ async deleteSponsorBlock(player) {
690
+ // no plugin enabled
691
+ if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
692
+ throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
693
+ // do the request
694
+ await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, (request) => {
695
+ request.method = "DELETE";
696
+ return request;
697
+ });
698
+ return;
699
+ }
631
700
  async queueEnd(player, track, payload) {
632
701
  // add previous track to the queue!
633
702
  player.queue.current = null;
@@ -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(): 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
@@ -192,7 +205,7 @@ export declare class Player {
192
205
  /**
193
206
  * Destroy the player and disconnect from the voice channel
194
207
  */
195
- destroy(reason?: string): Promise<this>;
208
+ destroy(reason?: string, disconnect?: boolean): Promise<this>;
196
209
  /**
197
210
  * Move the player on a different Audio-Node
198
211
  * @param newNode New Node / New Node Id
@@ -215,6 +215,15 @@ class Player {
215
215
  async lavaSearch(query, requestUser) {
216
216
  return this.node.lavaSearch(query, requestUser);
217
217
  }
218
+ async setSponsorBlock(segments = ["sponsor", "selfpromo"]) {
219
+ return this.node.setSponsorBlock(this, segments);
220
+ }
221
+ async getSponsorBlock() {
222
+ return this.node.getSponsorBlock(this);
223
+ }
224
+ async deleteSponsorBlock() {
225
+ return this.node.deleteSponsorBlock(this);
226
+ }
218
227
  /**
219
228
  *
220
229
  * @param query Query for your data
@@ -222,10 +231,6 @@ class Player {
222
231
  */
223
232
  async search(query, requestUser) {
224
233
  const Query = this.LavalinkManager.utils.transformQuery(query);
225
- if (/^https?:\/\//.test(Query.query))
226
- this.LavalinkManager.utils.validateQueryString(this.node, Query.source);
227
- else if (Query.source)
228
- this.LavalinkManager.utils.validateSourceString(this.node, Query.source);
229
234
  if (["bcsearch", "bandcamp"].includes(Query.source))
230
235
  return await (0, BandCampSearch_1.bandCampSearch)(this, Query.query, requestUser);
231
236
  return this.node.search(Query, requestUser);
@@ -289,8 +294,8 @@ class Player {
289
294
  * Skip the current song, or a specific amount of songs
290
295
  * @param amount provide the index of the next track to skip to
291
296
  */
292
- async skip(skipTo = 0) {
293
- if (!this.queue.tracks.length)
297
+ async skip(skipTo = 0, throwError = true) {
298
+ if (!this.queue.tracks.length && (throwError || (typeof skipTo === "boolean" && skipTo === true)))
294
299
  throw new RangeError("Can't skip more than the queue size");
295
300
  if (typeof skipTo === "number" && skipTo > 1) {
296
301
  if (skipTo > this.queue.tracks.length)
@@ -304,13 +309,27 @@ class Player {
304
309
  this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
305
310
  return this;
306
311
  }
312
+ /**
313
+ * Clears the queue and stops playing. Does not destroy the Player and not leave the channel
314
+ * @returns
315
+ */
316
+ async stopPlaying() {
317
+ // remove tracks from the queue
318
+ if (this.queue.tracks.length)
319
+ await this.queue.splice(0, this.queue.tracks.length);
320
+ const now = performance.now();
321
+ // send to lavalink, that it should stop playing
322
+ await this.node.updatePlayer({ guildId: this.guildId, playerOptions: { encodedTrack: null } });
323
+ this.ping.lavalink = Math.round((performance.now() - now) / 10) / 100;
324
+ return this;
325
+ }
307
326
  /**
308
327
  * Connects the Player to the Voice Channel
309
328
  * @returns
310
329
  */
311
330
  async connect() {
312
331
  if (!this.options.voiceChannelId)
313
- throw new RangeError("No Voice Channel id has been set.");
332
+ throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
314
333
  await this.LavalinkManager.options.sendToShard(this.guildId, {
315
334
  op: 4,
316
335
  d: {
@@ -320,6 +339,26 @@ class Player {
320
339
  self_deaf: this.options.selfDeaf ?? true,
321
340
  }
322
341
  });
342
+ this.voiceChannelId = this.options.voiceChannelId;
343
+ return this;
344
+ }
345
+ async changeVoiceState(data) {
346
+ if (this.options.voiceChannelId === data.voiceChannelId)
347
+ throw new RangeError("New Channel can't be equal to the old Channel.");
348
+ await this.LavalinkManager.options.sendToShard(this.guildId, {
349
+ op: 4,
350
+ d: {
351
+ guild_id: this.guildId,
352
+ channel_id: data.voiceChannelId,
353
+ self_mute: data.selfMute ?? this.options.selfMute ?? false,
354
+ self_deaf: data.selfDeaf ?? this.options.selfDeaf ?? true,
355
+ }
356
+ });
357
+ // override the options
358
+ this.options.voiceChannelId = data.voiceChannelId;
359
+ this.options.selfMute = data.selfMute;
360
+ this.options.selfDeaf = data.selfDeaf;
361
+ this.voiceChannelId = data.voiceChannelId;
323
362
  return this;
324
363
  }
325
364
  /**
@@ -329,7 +368,7 @@ class Player {
329
368
  */
330
369
  async disconnect(force = false) {
331
370
  if (!force && !this.options.voiceChannelId)
332
- throw new RangeError("No Voice Channel id has been set.");
371
+ throw new RangeError("No Voice Channel id has been set. (player.options.voiceChannelId)");
333
372
  await this.LavalinkManager.options.sendToShard(this.guildId, {
334
373
  op: 4,
335
374
  d: {
@@ -345,24 +384,27 @@ class Player {
345
384
  /**
346
385
  * Destroy the player and disconnect from the voice channel
347
386
  */
348
- async destroy(reason) {
349
- if (this.LavalinkManager.options.debugOptions.playerDestroy.debugLog)
387
+ async destroy(reason, disconnect = true) {
388
+ if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
350
389
  console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Destroy-Reason: ${String(reason)}`);
351
390
  if (this.get("internal_destroystatus") === true) {
352
- if (this.LavalinkManager.options.debugOptions.playerDestroy.debugLog)
391
+ if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
353
392
  console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Already destroying somewhere else..`);
354
393
  return;
355
394
  }
356
395
  this.set("internal_destroystatus", true);
357
396
  // disconnect player and set VoiceChannel to Null
358
- await this.disconnect(true);
397
+ if (disconnect)
398
+ await this.disconnect(true);
399
+ else
400
+ this.set("internal_destroywithoutdisconnect", true);
359
401
  // Destroy the queue
360
402
  await this.queue.utils.destroy();
361
403
  // delete the player from cache
362
- this.LavalinkManager.deletePlayer(this.guildId, !this.LavalinkManager.options.debugOptions.playerDestroy.dontThrowError);
404
+ this.LavalinkManager.deletePlayer(this.guildId);
363
405
  // destroy the player on lavalink side
364
406
  await this.node.destroyPlayer(this.guildId);
365
- if (this.LavalinkManager.options.debugOptions.playerDestroy.debugLog)
407
+ if (this.LavalinkManager.options.advancedOptions?.debugOptions.playerDestroy.debugLog)
366
408
  console.log(`Lavalink-Client-Debug | PlayerDestroy [::] destroy Function, [guildId ${this.guildId}] - Player got destroyed successfully`);
367
409
  // emit the event
368
410
  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;