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.
@@ -152,7 +152,7 @@ export declare class MiniMap<K, V> extends Map<K, V> {
152
152
  map<T>(fn: (value: V, key: K, miniMap: this) => T): T[];
153
153
  map<This, T>(fn: (this: This, value: V, key: K, miniMap: this) => T, thisArg: This): T[];
154
154
  }
155
- export type PlayerEvents = TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent;
155
+ export type PlayerEvents = TrackStartEvent | TrackEndEvent | TrackStuckEvent | TrackExceptionEvent | WebSocketClosedEvent | SponsorBlockSegmentEvents;
156
156
  export type Severity = "COMMON" | "SUSPICIOUS" | "FAULT";
157
157
  export interface Exception {
158
158
  severity: Severity;
@@ -188,9 +188,52 @@ export interface WebSocketClosedEvent extends PlayerEvent {
188
188
  byRemote: boolean;
189
189
  reason: string;
190
190
  }
191
+ /**
192
+ * Types & Events for Sponsorblock-plugin from Lavalink: https://github.com/topi314/Sponsorblock-Plugin#segmentsloaded
193
+ */
194
+ export type SponsorBlockSegmentEvents = SponsorBlockSegmentSkipped | SponsorBlockSegmentsLoaded | SponsorBlockChapterStarted | SponsorBlockChaptersLoaded;
195
+ export type SponsorBlockSegmentEventType = "SegmentSkipped" | "SegmentsLoaded" | "ChaptersLoaded" | "ChapterStarted";
196
+ export interface SponsorBlockSegmentsLoaded extends PlayerEvent {
197
+ type: "SegmentsLoaded";
198
+ segments: {
199
+ category: string;
200
+ start: number;
201
+ end: number;
202
+ }[];
203
+ }
204
+ export interface SponsorBlockSegmentSkipped extends PlayerEvent {
205
+ type: "SegmentSkipped";
206
+ segment: {
207
+ category: string;
208
+ start: number;
209
+ end: number;
210
+ };
211
+ }
212
+ export interface SponsorBlockChapterStarted extends PlayerEvent {
213
+ type: "ChapterStarted";
214
+ /** The Chapter which started */
215
+ chapter: {
216
+ /** The Name of the Chapter */
217
+ name: string;
218
+ start: number;
219
+ end: number;
220
+ duration: number;
221
+ };
222
+ }
223
+ export interface SponsorBlockChaptersLoaded extends PlayerEvent {
224
+ type: "ChaptersLoaded";
225
+ /** All Chapters loaded */
226
+ chapters: {
227
+ /** The Name of the Chapter */
228
+ name: string;
229
+ start: number;
230
+ end: number;
231
+ duration: number;
232
+ }[];
233
+ }
191
234
  export type LoadTypes = "track" | "playlist" | "search" | "error" | "empty";
192
235
  export type State = "CONNECTED" | "CONNECTING" | "DISCONNECTED" | "DISCONNECTING" | "DESTROYING";
193
- export type PlayerEventType = "TrackStartEvent" | "TrackEndEvent" | "TrackExceptionEvent" | "TrackStuckEvent" | "WebSocketClosedEvent";
236
+ export type PlayerEventType = "TrackStartEvent" | "TrackEndEvent" | "TrackExceptionEvent" | "TrackStuckEvent" | "WebSocketClosedEvent" | SponsorBlockSegmentEventType;
194
237
  export type TrackEndReason = "finished" | "loadFailed" | "stopped" | "replaced" | "cleanup";
195
238
  export interface InvalidLavalinkRestRequest {
196
239
  timestamp: number;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.queueTrackEnd = exports.MiniMap = exports.ManagerUtils = exports.NodeSymbol = exports.QueueSymbol = exports.UnresolvedTrackSymbol = exports.TrackSymbol = void 0;
4
+ const types_1 = require("util/types");
4
5
  const LavalinkManagerStatics_1 = require("./LavalinkManagerStatics");
5
6
  exports.TrackSymbol = Symbol("LC-Track");
6
7
  exports.UnresolvedTrackSymbol = Symbol("LC-Track-Unresolved");
@@ -166,6 +167,18 @@ class ManagerUtils {
166
167
  throw new Error("No Lavalink Node was provided");
167
168
  if (!node.info.sourceManagers?.length)
168
169
  throw new Error("Lavalink Node, has no sourceManagers enabled");
170
+ // checks for blacklisted links / domains / queries
171
+ 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()))) || (0, types_1.isRegExp)(v) && v.test(queryString))) {
172
+ throw new Error(`Query string contains a link / word which is blacklisted.`);
173
+ }
174
+ if (!/^https?:\/\//.test(queryString))
175
+ return;
176
+ else if (this.LavalinkManager.options?.linksAllowed === false)
177
+ throw new Error("Using links to make a request is not allowed.");
178
+ // checks for if the query is whitelisted (should only work for links, so it skips the check for no link queries)
179
+ 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()))) || (0, types_1.isRegExp)(v) && v.test(queryString))) {
180
+ throw new Error(`Query string contains a link / word which isn't whitelisted.`);
181
+ }
169
182
  // missing links: beam.pro local getyarn.io clypit pornhub reddit ocreamix soundgasm
170
183
  if ((LavalinkManagerStatics_1.SourceLinksRegexes.YoutubeMusicRegex.test(queryString) || LavalinkManagerStatics_1.SourceLinksRegexes.YoutubeRegex.test(queryString)) && !node.info?.sourceManagers?.includes("youtube")) {
171
184
  throw new Error("Lavalink Node has not 'youtube' enabled");
@@ -2,6 +2,7 @@ import { fetch } from "undici";
2
2
  export const bandCampSearch = async (player, query, requestUser) => {
3
3
  let error = null;
4
4
  let tracks = [];
5
+ player.LavalinkManager.utils.validateQueryString(player.node, query);
5
6
  try {
6
7
  const data = await fetch(`https://bandcamp.com/api/nusearch/2/autocomplete?q=${encodeURIComponent(query)}`, {
7
8
  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.
@@ -34,17 +34,22 @@ export class LavalinkManager extends EventEmitter {
34
34
  requesterTransformer: options?.playerOptions?.requesterTransformer ?? null,
35
35
  useUnresolvedData: options?.playerOptions?.useUnresolvedData ?? false,
36
36
  },
37
+ linksWhitelist: options?.linksWhitelist ?? [],
38
+ linksBlacklist: options?.linksBlacklist ?? [],
37
39
  autoSkip: options?.autoSkip ?? true,
40
+ emitNewSongsOnly: options?.emitNewSongsOnly ?? false,
38
41
  queueOptions: {
39
42
  maxPreviousTracks: options?.queueOptions?.maxPreviousTracks ?? 25,
40
43
  queueChangesWatcher: options?.queueOptions?.queueChangesWatcher ?? null,
41
44
  queueStore: options?.queueOptions?.queueStore ?? new DefaultQueueStore(),
42
45
  },
43
- debugOptions: {
44
- noAudio: options?.debugOptions?.noAudio ?? false,
45
- playerDestroy: {
46
- dontThrowError: options?.debugOptions?.playerDestroy?.dontThrowError ?? false,
47
- debugLog: options?.debugOptions?.playerDestroy?.debugLog ?? false,
46
+ advancedOptions: {
47
+ debugOptions: {
48
+ noAudio: options?.advancedOptions?.debugOptions?.noAudio ?? false,
49
+ playerDestroy: {
50
+ dontThrowError: options?.advancedOptions?.debugOptions?.playerDestroy?.dontThrowError ?? false,
51
+ debugLog: options?.advancedOptions?.debugOptions?.playerDestroy?.debugLog ?? false,
52
+ }
48
53
  }
49
54
  }
50
55
  };
@@ -57,6 +62,8 @@ export class LavalinkManager extends EventEmitter {
57
62
  // 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");
58
63
  if (options?.autoSkip && typeof options?.autoSkip !== "boolean")
59
64
  throw new SyntaxError("ManagerOption.autoSkip must be either false | true aka boolean");
65
+ if (options?.emitNewSongsOnly && typeof options?.emitNewSongsOnly !== "boolean")
66
+ throw new SyntaxError("ManagerOption.emitNewSongsOnly must be either false | true aka boolean");
60
67
  if (!options?.nodes || !Array.isArray(options?.nodes) || !options?.nodes.every(node => this.utils.isNodeOptions(node)))
61
68
  throw new SyntaxError("ManagerOption.nodes must be an Array of NodeOptions and is required of at least 1 Node");
62
69
  /* QUEUE STORE */
@@ -98,15 +105,22 @@ export class LavalinkManager extends EventEmitter {
98
105
  getPlayer(guildId) {
99
106
  return this.players.get(guildId);
100
107
  }
101
- deletePlayer(guildId, throwError = true) {
108
+ destroyPlayer(guildId, destroyReason) {
109
+ const oldPlayer = this.getPlayer(guildId);
110
+ if (!oldPlayer)
111
+ return;
112
+ return oldPlayer.destroy(destroyReason);
113
+ }
114
+ deletePlayer(guildId) {
102
115
  const oldPlayer = this.getPlayer(guildId);
103
116
  if (!oldPlayer)
104
117
  return;
105
- if (oldPlayer.voiceChannelId === "string" && oldPlayer.connected) {
106
- if (throwError)
107
- throw new Error(`Use Player#destroy(true) not PlayerManager#deletePlayer() to stop the Player ${JSON.stringify(oldPlayer.toJSON?.())}`);
118
+ // oldPlayer.connected is operational. you could also do oldPlayer.voice?.token
119
+ if (oldPlayer.voiceChannelId === "string" && oldPlayer.connected && !oldPlayer.get("internal_destroywithoutdisconnect")) {
120
+ if (!this.options?.advancedOptions?.debugOptions?.playerDestroy?.dontThrowError)
121
+ throw new Error(`Use Player#destroy() not LavalinkManager#deletePlayer() to stop the Player ${JSON.stringify(oldPlayer.toJSON?.())}`);
108
122
  else
109
- console.error("Use Player#destroy(true) not PlayerManager#deletePlayer() to stop the Player", oldPlayer.toJSON?.());
123
+ console.error("Use Player#destroy() not LavalinkManager#deletePlayer() to stop the Player", oldPlayer.toJSON?.());
110
124
  }
111
125
  return this.players.delete(guildId);
112
126
  }
@@ -149,12 +163,12 @@ export class LavalinkManager extends EventEmitter {
149
163
  */
150
164
  async sendRawData(data) {
151
165
  if (!this.initiated) {
152
- if (this.options?.debugOptions?.noAudio === true)
166
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
153
167
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, manager is not initated yet");
154
168
  return;
155
169
  }
156
170
  if (!("t" in data)) {
157
- if (this.options?.debugOptions?.noAudio === true)
171
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
158
172
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no 't' in payload-data of the raw event:", data);
159
173
  return;
160
174
  }
@@ -171,23 +185,23 @@ export class LavalinkManager extends EventEmitter {
171
185
  if (["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t)) {
172
186
  const update = ("d" in data ? data.d : data);
173
187
  if (!update) {
174
- if (this.options?.debugOptions?.noAudio === true)
188
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
175
189
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no update data found in payload:", data);
176
190
  return;
177
191
  }
178
192
  if (!("token" in update) && !("session_id" in update)) {
179
- if (this.options?.debugOptions?.noAudio === true)
193
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
180
194
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, no 'token' nor 'session_id' found in payload:", data);
181
195
  return;
182
196
  }
183
197
  const player = this.getPlayer(update.guild_id);
184
198
  if (!player) {
185
- if (this.options?.debugOptions?.noAudio === true)
199
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
186
200
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, No Lavalink Player found via key: 'guild_id' of update-data:", update);
187
201
  return;
188
202
  }
189
203
  if (player.get("internal_destroystatus") === true) {
190
- if (this.options?.debugOptions?.noAudio === true)
204
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
191
205
  console.debug("Lavalink-Client-Debug | NO-AUDIO [::] sendRawData function, Player is in a destroying state. can't signal the voice states");
192
206
  return;
193
207
  }
@@ -204,13 +218,13 @@ export class LavalinkManager extends EventEmitter {
204
218
  }
205
219
  }
206
220
  });
207
- if (this.options?.debugOptions?.noAudio === true)
221
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
208
222
  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, } });
209
223
  return;
210
224
  }
211
225
  /* voice state update */
212
226
  if (update.user_id !== this.options?.client.id) {
213
- if (this.options?.debugOptions?.noAudio === true)
227
+ if (this.options?.advancedOptions?.debugOptions?.noAudio === true)
214
228
  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);
215
229
  return;
216
230
  }
@@ -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;
@@ -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;
@@ -83,7 +84,7 @@ export class LavalinkNode {
83
84
  modify?.(options);
84
85
  const url = new URL(`${this.poolAddress}${options.path}`);
85
86
  url.searchParams.append("trace", "true");
86
- options.path = url.toString().replace(this.poolAddress, "");
87
+ options.path = url.pathname + url.search;
87
88
  const request = await this.rest.request(options);
88
89
  this.calls++;
89
90
  if (options.method === "DELETE")
@@ -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!");
@@ -167,7 +167,7 @@ export class LavalinkNode {
167
167
  if (data.noReplace) {
168
168
  const url = new URL(`${this.poolAddress}${r.path}`);
169
169
  url.searchParams.append("noReplace", data.noReplace?.toString() || "false");
170
- r.path = url.toString().replace(this.poolAddress, "");
170
+ r.path = url.pathname + url.search;
171
171
  }
172
172
  });
173
173
  return this.syncPlayerData({}, res), res;
@@ -584,15 +584,42 @@ export class LavalinkNode {
584
584
  case "WebSocketClosedEvent":
585
585
  this.socketClosed(player, payload);
586
586
  break;
587
+ case "SegmentsLoaded":
588
+ this.SponsorBlockSegmentLoaded(player, player.queue.current, payload);
589
+ break;
590
+ case "SegmentSkipped":
591
+ this.SponsorBlockSegmentkipped(player, player.queue.current, payload);
592
+ break;
593
+ case "ChaptersLoaded":
594
+ this.SponsorBlockChaptersLoaded(player, player.queue.current, payload);
595
+ break;
596
+ case "ChapterStarted":
597
+ this.SponsorBlockChapterStarted(player, player.queue.current, payload);
598
+ break;
587
599
  default:
588
600
  this.NodeManager.emit("error", this, new Error(`Node#event unknown event '${payload.type}'.`), payload);
589
601
  break;
590
602
  }
591
603
  return;
592
604
  }
605
+ SponsorBlockSegmentLoaded(player, track, payload) {
606
+ return this.NodeManager.LavalinkManager.emit("SegmentsLoaded", player, track, payload);
607
+ }
608
+ SponsorBlockSegmentkipped(player, track, payload) {
609
+ return this.NodeManager.LavalinkManager.emit("SegmentSkipped", player, track, payload);
610
+ }
611
+ SponsorBlockChaptersLoaded(player, track, payload) {
612
+ return this.NodeManager.LavalinkManager.emit("ChaptersLoaded", player, track, payload);
613
+ }
614
+ SponsorBlockChapterStarted(player, track, payload) {
615
+ return this.NodeManager.LavalinkManager.emit("ChapterStarted", player, track, payload);
616
+ }
593
617
  trackStart(player, track, payload) {
594
618
  player.playing = true;
595
619
  player.paused = false;
620
+ // don't emit the event if previous track == new track aka track loop
621
+ if (this.NodeManager.LavalinkManager.options?.emitNewSongsOnly === true && player.queue.previous[0]?.info?.identifier === track?.info?.identifier)
622
+ return;
596
623
  return this.NodeManager.LavalinkManager.emit("trackStart", player, track, payload);
597
624
  }
598
625
  async trackEnd(player, track, payload) {
@@ -616,6 +643,12 @@ export class LavalinkNode {
616
643
  // remove tracks from the queue
617
644
  if (player.repeatMode !== "track")
618
645
  await queueTrackEnd(player);
646
+ else if (player.queue.current) { // If there was a current Track already and repeatmode === true, add it to the queue.
647
+ player.queue.previous.unshift(player.queue.current);
648
+ if (player.queue.previous.length > player.queue.options.maxPreviousTracks)
649
+ player.queue.previous.splice(player.queue.options.maxPreviousTracks, player.queue.previous.length);
650
+ await player.queue.utils.save();
651
+ }
619
652
  // if no track available, end queue
620
653
  if (!player.queue.current)
621
654
  return this.queueEnd(player, track, payload);
@@ -624,6 +657,42 @@ export class LavalinkNode {
624
657
  // play track if autoSkip is true
625
658
  return this.NodeManager.LavalinkManager.options.autoSkip && player.play({ noReplace: true });
626
659
  }
660
+ async getSponsorBlock(player) {
661
+ // no plugin enabled
662
+ if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
663
+ throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
664
+ // do the request
665
+ return await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`);
666
+ }
667
+ async setSponsorBlock(player, segments = ["sponsor", "selfpromo"]) {
668
+ // no plugin enabled
669
+ if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
670
+ throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
671
+ // no segments length
672
+ if (!segments.length)
673
+ throw new RangeError("No Segments provided. Did you ment to use 'deleteSponsorBlock'?");
674
+ // a not valid segment
675
+ if (segments.some(v => !validSponsorBlocks.includes(v.toLowerCase())))
676
+ throw new SyntaxError(`You provided a sponsorblock which isn't valid, valid ones are: ${validSponsorBlocks.map(v => `'${v}'`).join(", ")}`);
677
+ // do the request
678
+ await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, (request) => {
679
+ request.method = "PUT";
680
+ request.body = JSON.stringify(segments.map(v => v.toLowerCase()));
681
+ return request;
682
+ });
683
+ return;
684
+ }
685
+ async deleteSponsorBlock(player) {
686
+ // no plugin enabled
687
+ if (!this.info.plugins.find(v => v.name === "sponsorblock-plugin"))
688
+ throw new RangeError(`there is no sponsorblock-plugin available in the lavalink node: ${this.id}`);
689
+ // do the request
690
+ await this.request(`/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, (request) => {
691
+ request.method = "DELETE";
692
+ return request;
693
+ });
694
+ return;
695
+ }
627
696
  async queueEnd(player, track, payload) {
628
697
  // add previous track to the queue!
629
698
  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