lavalink-client 1.0.5 → 1.0.6

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
@@ -11,6 +11,7 @@ Latest stable Version: (currently, unreleased)
11
11
  npm install --save lavalink-client
12
12
  ```
13
13
 
14
+
14
15
  Dev Version: (Current)
15
16
 
16
17
  ```bash
@@ -11,7 +11,9 @@ export interface LavalinkManager {
11
11
  utils: ManagerUitls;
12
12
  }
13
13
  export interface BotClientOptions {
14
+ /** Bot Client Id */
14
15
  id: string;
16
+ /** Bot Client Username */
15
17
  username?: string;
16
18
  /** So users can pass entire objects / classes */
17
19
  [x: string | number | symbol | undefined]: any;
@@ -39,6 +41,7 @@ export interface LavalinkPlayerOptions {
39
41
  autoPlayFunction?: (player: Player, lastPlayedTrack: Track) => Promise<void>;
40
42
  destroyAfterMs?: number;
41
43
  };
44
+ useUnresolvedData?: boolean;
42
45
  }
43
46
  export interface ManagerOptions {
44
47
  nodes: LavalinkNodeOptions[];
@@ -4,7 +4,7 @@ import { NodeManager } from "./NodeManager";
4
4
  import internal from "stream";
5
5
  import { InvalidLavalinkRestRequest, LavalinkPlayer, PlayerUpdateInfo, RoutePlanner, Session, Base64 } from "./Utils";
6
6
  import { DestroyReasonsType } from "./Player";
7
- import { TrackInfo } from "./Track";
7
+ import { Track } from "./Track";
8
8
  /** Modifies any outgoing REST requests. */
9
9
  export type ModifyRequest = (options: Dispatcher.RequestOptions) => void;
10
10
  export interface LavalinkNodeOptions {
@@ -185,13 +185,13 @@ export declare class LavalinkNode {
185
185
  * @param encoded
186
186
  * @returns
187
187
  */
188
- singleTrack: (encoded: Base64) => Promise<TrackInfo>;
188
+ singleTrack: (encoded: Base64, requester: unknown) => Promise<Track>;
189
189
  /**
190
190
  *
191
191
  * @param encodeds Decodes multiple tracks into their info
192
192
  * @returns
193
193
  */
194
- multipleTracks: (encodeds: Base64[]) => Promise<TrackInfo[]>;
194
+ multipleTracks: (encodeds: Base64[], requester: unknown) => Promise<Track[]>;
195
195
  };
196
196
  /**
197
197
  * Request Lavalink statistics.
@@ -227,25 +227,27 @@ class LavalinkNode {
227
227
  * @param encoded
228
228
  * @returns
229
229
  */
230
- singleTrack: async (encoded) => {
230
+ singleTrack: async (encoded, requester) => {
231
231
  if (!encoded)
232
232
  throw new SyntaxError("No encoded (Base64 string) was provided");
233
- return await this.request(`/decodetrack?encodedTrack=${encoded}`);
233
+ // return the decoded + builded track
234
+ return this.NodeManager.LavalinkManager.utils.buildTrack(await this.request(`/decodetrack?encodedTrack=${encoded}`), requester);
234
235
  },
235
236
  /**
236
237
  *
237
238
  * @param encodeds Decodes multiple tracks into their info
238
239
  * @returns
239
240
  */
240
- multipleTracks: async (encodeds) => {
241
+ multipleTracks: async (encodeds, requester) => {
241
242
  if (!Array.isArray(encodeds) || !encodeds.every(v => typeof v === "string" && v.length > 1))
242
243
  throw new SyntaxError("You need to provide encodeds, which is an array of base64 strings");
244
+ // return the decoded + builded tracks
243
245
  return await this.request(`/decodetracks`, r => {
244
246
  r.method = "POST";
245
247
  r.body = JSON.stringify(encodeds);
246
248
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247
249
  r.headers["Content-Type"] = "application/json";
248
- });
250
+ }).then((r) => r.map(track => this.NodeManager.LavalinkManager.utils.buildTrack(track, requester)));
249
251
  }
250
252
  };
251
253
  /**
@@ -6,6 +6,8 @@ exports.TrackSymbol = Symbol("LC-Track");
6
6
  exports.UnresolvedTrackSymbol = Symbol("LC-Track-Unresolved");
7
7
  exports.QueueSymbol = Symbol("LC-Queue");
8
8
  exports.NodeSymbol = Symbol("LC-Node");
9
+ /** @hidden */
10
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
11
  class ManagerUitls {
10
12
  constructor(LavalinkManager) {
11
13
  this.manager = LavalinkManager;
@@ -23,7 +25,7 @@ class ManagerUitls {
23
25
  identifier: data.info?.identifier,
24
26
  title: data.info?.title,
25
27
  author: data.info?.author,
26
- duration: data.info?.length,
28
+ duration: data.info?.length || data.info?.duration,
27
29
  artworkUrl: data.info?.artworkUrl || data.pluginInfo?.artworkUrl || data.plugin?.artworkUrl,
28
30
  uri: data.info?.uri,
29
31
  sourceName: data.info?.sourceName,
@@ -95,13 +97,21 @@ class ManagerUitls {
95
97
  * @returns
96
98
  */
97
99
  isTrack(data) {
98
- return typeof data?.encoded === "string" && typeof data?.info === "object";
100
+ if (!data)
101
+ return false;
102
+ if (data[exports.TrackSymbol] === true)
103
+ return true;
104
+ return typeof data?.encoded === "string" && typeof data?.info === "object" && !("resolve" in data);
99
105
  }
100
106
  /**
101
107
  * Checks if the provided argument is a valid UnresolvedTrack.
102
108
  * @param track
103
109
  */
104
110
  isUnresolvedTrack(data) {
111
+ if (!data)
112
+ return false;
113
+ if (data[exports.UnresolvedTrackSymbol] === true)
114
+ return true;
105
115
  return typeof data === "object" && "info" in data && typeof data.info.title === "string" && typeof data.resolve === "function";
106
116
  }
107
117
  /**
@@ -277,6 +287,32 @@ async function queueTrackEnd(player) {
277
287
  return player.queue.current;
278
288
  }
279
289
  exports.queueTrackEnd = queueTrackEnd;
290
+ async function applyUnresolvedData(resTrack, data, utils) {
291
+ if (!resTrack?.info || !data?.info)
292
+ return;
293
+ if (utils.manager.options.playerOptions?.useUnresolvedData === true) { // overwrite values
294
+ if (data.info.uri)
295
+ resTrack.info.uri = data.info.uri;
296
+ if (data.info.artworkUrl?.length)
297
+ resTrack.info.artworkUrl = data.info.artworkUrl;
298
+ if (data.info.title?.length)
299
+ resTrack.info.title = data.info.title;
300
+ if (data.info.author?.length)
301
+ resTrack.info.author = data.info.author;
302
+ }
303
+ else { // only overwrite if undefined / invalid
304
+ if ((resTrack.info.title == 'Unknown title' || resTrack.info.title == "Unspecified description") && resTrack.info.title != data.info.title)
305
+ resTrack.info.title = data.info.title;
306
+ if (resTrack.info.author != data.info.author)
307
+ resTrack.info.author = data.info.author;
308
+ if (resTrack.info.artworkUrl != data.info.artworkUrl)
309
+ resTrack.info.artworkUrl = data.info.artworkUrl;
310
+ }
311
+ for (const key of Object.keys(data.info))
312
+ if (typeof resTrack.info[key] === "undefined" && key !== "resolve" && data.info[key])
313
+ resTrack.info[key] = data.info[key]; // add non-existing values
314
+ return resTrack;
315
+ }
280
316
  async function getClosestTrack(data, player, utils) {
281
317
  if (!player || !player.node)
282
318
  throw new RangeError("No player with a lavalink node was provided");
@@ -288,19 +324,35 @@ async function getClosestTrack(data, player, utils) {
288
324
  throw new SyntaxError("the track title is required for unresolved tracks");
289
325
  if (!data.requester)
290
326
  throw new SyntaxError("The requester is required");
327
+ // try to decode the track, if possible
291
328
  if (typeof data.encoded === "string") {
292
- const r = await player.node.decode.singleTrack(data.encoded);
329
+ const r = await player.node.decode.singleTrack(data.encoded, data.requester);
293
330
  if (r)
294
- return utils.buildTrack(r, data.requester);
331
+ return applyUnresolvedData(r, data, utils);
295
332
  }
333
+ // try to fetch the track via a uri if possible
296
334
  if (typeof data.info.uri === "string") {
297
335
  const r = await player.search({ query: data?.info?.uri }, data.requester).then(v => v.tracks[0]);
298
336
  if (r)
299
- return r;
337
+ return applyUnresolvedData(r, data, utils);
300
338
  }
339
+ // search the track as closely as possible
301
340
  const query = [data.info?.title, data.info?.author].filter(str => !!str).join(" by ");
302
341
  const sourceName = data.info?.sourceName;
303
342
  return await player.search({
304
343
  query, source: sourceName !== "bandcamp" && sourceName !== "twitch" && sourceName !== "flowery-tts" ? sourceName : player.LavalinkManager.options?.playerOptions?.defaultSearchPlatform,
305
- }, data.requester).then(v => v.tracks[0]);
344
+ }, data.requester).then(res => {
345
+ let trackToUse = null;
346
+ // try to find via author name
347
+ if (data.info.author && !trackToUse)
348
+ trackToUse = res.tracks.find(track => [data.info.author, `${data.info.author} - Topic`].some(name => new RegExp(`^${escapeRegExp(name)}$`, "i").test(track.info.author)) || new RegExp(`^${escapeRegExp(data.info.title)}$`, "i").test(track.info.title));
349
+ // try to find via duration
350
+ if (data.info.duration && !trackToUse)
351
+ trackToUse = res.tracks.find(track => (track.info.duration >= (data.info.duration - 1500)) && (track.info.duration <= (data.info.duration + 1500)));
352
+ // try to find via isrc
353
+ if (data.info.isrc && !trackToUse)
354
+ trackToUse = res.tracks.find(track => track.info.isrc === data.info.isrc);
355
+ // apply unresolved data and return the track
356
+ return applyUnresolvedData(trackToUse || res.tracks[0], data, utils);
357
+ });
306
358
  }
@@ -11,7 +11,9 @@ export interface LavalinkManager {
11
11
  utils: ManagerUitls;
12
12
  }
13
13
  export interface BotClientOptions {
14
+ /** Bot Client Id */
14
15
  id: string;
16
+ /** Bot Client Username */
15
17
  username?: string;
16
18
  /** So users can pass entire objects / classes */
17
19
  [x: string | number | symbol | undefined]: any;
@@ -39,6 +41,7 @@ export interface LavalinkPlayerOptions {
39
41
  autoPlayFunction?: (player: Player, lastPlayedTrack: Track) => Promise<void>;
40
42
  destroyAfterMs?: number;
41
43
  };
44
+ useUnresolvedData?: boolean;
42
45
  }
43
46
  export interface ManagerOptions {
44
47
  nodes: LavalinkNodeOptions[];
@@ -4,7 +4,7 @@ import { NodeManager } from "./NodeManager";
4
4
  import internal from "stream";
5
5
  import { InvalidLavalinkRestRequest, LavalinkPlayer, PlayerUpdateInfo, RoutePlanner, Session, Base64 } from "./Utils";
6
6
  import { DestroyReasonsType } from "./Player";
7
- import { TrackInfo } from "./Track";
7
+ import { Track } from "./Track";
8
8
  /** Modifies any outgoing REST requests. */
9
9
  export type ModifyRequest = (options: Dispatcher.RequestOptions) => void;
10
10
  export interface LavalinkNodeOptions {
@@ -185,13 +185,13 @@ export declare class LavalinkNode {
185
185
  * @param encoded
186
186
  * @returns
187
187
  */
188
- singleTrack: (encoded: Base64) => Promise<TrackInfo>;
188
+ singleTrack: (encoded: Base64, requester: unknown) => Promise<Track>;
189
189
  /**
190
190
  *
191
191
  * @param encodeds Decodes multiple tracks into their info
192
192
  * @returns
193
193
  */
194
- multipleTracks: (encodeds: Base64[]) => Promise<TrackInfo[]>;
194
+ multipleTracks: (encodeds: Base64[], requester: unknown) => Promise<Track[]>;
195
195
  };
196
196
  /**
197
197
  * Request Lavalink statistics.
@@ -223,25 +223,27 @@ export class LavalinkNode {
223
223
  * @param encoded
224
224
  * @returns
225
225
  */
226
- singleTrack: async (encoded) => {
226
+ singleTrack: async (encoded, requester) => {
227
227
  if (!encoded)
228
228
  throw new SyntaxError("No encoded (Base64 string) was provided");
229
- return await this.request(`/decodetrack?encodedTrack=${encoded}`);
229
+ // return the decoded + builded track
230
+ return this.NodeManager.LavalinkManager.utils.buildTrack(await this.request(`/decodetrack?encodedTrack=${encoded}`), requester);
230
231
  },
231
232
  /**
232
233
  *
233
234
  * @param encodeds Decodes multiple tracks into their info
234
235
  * @returns
235
236
  */
236
- multipleTracks: async (encodeds) => {
237
+ multipleTracks: async (encodeds, requester) => {
237
238
  if (!Array.isArray(encodeds) || !encodeds.every(v => typeof v === "string" && v.length > 1))
238
239
  throw new SyntaxError("You need to provide encodeds, which is an array of base64 strings");
240
+ // return the decoded + builded tracks
239
241
  return await this.request(`/decodetracks`, r => {
240
242
  r.method = "POST";
241
243
  r.body = JSON.stringify(encodeds);
242
244
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
243
245
  r.headers["Content-Type"] = "application/json";
244
- });
246
+ }).then((r) => r.map(track => this.NodeManager.LavalinkManager.utils.buildTrack(track, requester)));
245
247
  }
246
248
  };
247
249
  /**
@@ -3,6 +3,8 @@ export const TrackSymbol = Symbol("LC-Track");
3
3
  export const UnresolvedTrackSymbol = Symbol("LC-Track-Unresolved");
4
4
  export const QueueSymbol = Symbol("LC-Queue");
5
5
  export const NodeSymbol = Symbol("LC-Node");
6
+ /** @hidden */
7
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
8
  export class ManagerUitls {
7
9
  constructor(LavalinkManager) {
8
10
  this.manager = LavalinkManager;
@@ -20,7 +22,7 @@ export class ManagerUitls {
20
22
  identifier: data.info?.identifier,
21
23
  title: data.info?.title,
22
24
  author: data.info?.author,
23
- duration: data.info?.length,
25
+ duration: data.info?.length || data.info?.duration,
24
26
  artworkUrl: data.info?.artworkUrl || data.pluginInfo?.artworkUrl || data.plugin?.artworkUrl,
25
27
  uri: data.info?.uri,
26
28
  sourceName: data.info?.sourceName,
@@ -92,13 +94,21 @@ export class ManagerUitls {
92
94
  * @returns
93
95
  */
94
96
  isTrack(data) {
95
- return typeof data?.encoded === "string" && typeof data?.info === "object";
97
+ if (!data)
98
+ return false;
99
+ if (data[TrackSymbol] === true)
100
+ return true;
101
+ return typeof data?.encoded === "string" && typeof data?.info === "object" && !("resolve" in data);
96
102
  }
97
103
  /**
98
104
  * Checks if the provided argument is a valid UnresolvedTrack.
99
105
  * @param track
100
106
  */
101
107
  isUnresolvedTrack(data) {
108
+ if (!data)
109
+ return false;
110
+ if (data[UnresolvedTrackSymbol] === true)
111
+ return true;
102
112
  return typeof data === "object" && "info" in data && typeof data.info.title === "string" && typeof data.resolve === "function";
103
113
  }
104
114
  /**
@@ -271,6 +281,32 @@ export async function queueTrackEnd(player) {
271
281
  // return the new current Track
272
282
  return player.queue.current;
273
283
  }
284
+ async function applyUnresolvedData(resTrack, data, utils) {
285
+ if (!resTrack?.info || !data?.info)
286
+ return;
287
+ if (utils.manager.options.playerOptions?.useUnresolvedData === true) { // overwrite values
288
+ if (data.info.uri)
289
+ resTrack.info.uri = data.info.uri;
290
+ if (data.info.artworkUrl?.length)
291
+ resTrack.info.artworkUrl = data.info.artworkUrl;
292
+ if (data.info.title?.length)
293
+ resTrack.info.title = data.info.title;
294
+ if (data.info.author?.length)
295
+ resTrack.info.author = data.info.author;
296
+ }
297
+ else { // only overwrite if undefined / invalid
298
+ if ((resTrack.info.title == 'Unknown title' || resTrack.info.title == "Unspecified description") && resTrack.info.title != data.info.title)
299
+ resTrack.info.title = data.info.title;
300
+ if (resTrack.info.author != data.info.author)
301
+ resTrack.info.author = data.info.author;
302
+ if (resTrack.info.artworkUrl != data.info.artworkUrl)
303
+ resTrack.info.artworkUrl = data.info.artworkUrl;
304
+ }
305
+ for (const key of Object.keys(data.info))
306
+ if (typeof resTrack.info[key] === "undefined" && key !== "resolve" && data.info[key])
307
+ resTrack.info[key] = data.info[key]; // add non-existing values
308
+ return resTrack;
309
+ }
274
310
  async function getClosestTrack(data, player, utils) {
275
311
  if (!player || !player.node)
276
312
  throw new RangeError("No player with a lavalink node was provided");
@@ -282,19 +318,35 @@ async function getClosestTrack(data, player, utils) {
282
318
  throw new SyntaxError("the track title is required for unresolved tracks");
283
319
  if (!data.requester)
284
320
  throw new SyntaxError("The requester is required");
321
+ // try to decode the track, if possible
285
322
  if (typeof data.encoded === "string") {
286
- const r = await player.node.decode.singleTrack(data.encoded);
323
+ const r = await player.node.decode.singleTrack(data.encoded, data.requester);
287
324
  if (r)
288
- return utils.buildTrack(r, data.requester);
325
+ return applyUnresolvedData(r, data, utils);
289
326
  }
327
+ // try to fetch the track via a uri if possible
290
328
  if (typeof data.info.uri === "string") {
291
329
  const r = await player.search({ query: data?.info?.uri }, data.requester).then(v => v.tracks[0]);
292
330
  if (r)
293
- return r;
331
+ return applyUnresolvedData(r, data, utils);
294
332
  }
333
+ // search the track as closely as possible
295
334
  const query = [data.info?.title, data.info?.author].filter(str => !!str).join(" by ");
296
335
  const sourceName = data.info?.sourceName;
297
336
  return await player.search({
298
337
  query, source: sourceName !== "bandcamp" && sourceName !== "twitch" && sourceName !== "flowery-tts" ? sourceName : player.LavalinkManager.options?.playerOptions?.defaultSearchPlatform,
299
- }, data.requester).then(v => v.tracks[0]);
338
+ }, data.requester).then(res => {
339
+ let trackToUse = null;
340
+ // try to find via author name
341
+ if (data.info.author && !trackToUse)
342
+ trackToUse = res.tracks.find(track => [data.info.author, `${data.info.author} - Topic`].some(name => new RegExp(`^${escapeRegExp(name)}$`, "i").test(track.info.author)) || new RegExp(`^${escapeRegExp(data.info.title)}$`, "i").test(track.info.title));
343
+ // try to find via duration
344
+ if (data.info.duration && !trackToUse)
345
+ trackToUse = res.tracks.find(track => (track.info.duration >= (data.info.duration - 1500)) && (track.info.duration <= (data.info.duration + 1500)));
346
+ // try to find via isrc
347
+ if (data.info.isrc && !trackToUse)
348
+ trackToUse = res.tracks.find(track => track.info.isrc === data.info.isrc);
349
+ // apply unresolved data and return the track
350
+ return applyUnresolvedData(trackToUse || res.tracks[0], data, utils);
351
+ });
300
352
  }
@@ -11,7 +11,9 @@ export interface LavalinkManager {
11
11
  utils: ManagerUitls;
12
12
  }
13
13
  export interface BotClientOptions {
14
+ /** Bot Client Id */
14
15
  id: string;
16
+ /** Bot Client Username */
15
17
  username?: string;
16
18
  /** So users can pass entire objects / classes */
17
19
  [x: string | number | symbol | undefined]: any;
@@ -39,6 +41,7 @@ export interface LavalinkPlayerOptions {
39
41
  autoPlayFunction?: (player: Player, lastPlayedTrack: Track) => Promise<void>;
40
42
  destroyAfterMs?: number;
41
43
  };
44
+ useUnresolvedData?: boolean;
42
45
  }
43
46
  export interface ManagerOptions {
44
47
  nodes: LavalinkNodeOptions[];
@@ -4,7 +4,7 @@ import { NodeManager } from "./NodeManager";
4
4
  import internal from "stream";
5
5
  import { InvalidLavalinkRestRequest, LavalinkPlayer, PlayerUpdateInfo, RoutePlanner, Session, Base64 } from "./Utils";
6
6
  import { DestroyReasonsType } from "./Player";
7
- import { TrackInfo } from "./Track";
7
+ import { Track } from "./Track";
8
8
  /** Modifies any outgoing REST requests. */
9
9
  export type ModifyRequest = (options: Dispatcher.RequestOptions) => void;
10
10
  export interface LavalinkNodeOptions {
@@ -185,13 +185,13 @@ export declare class LavalinkNode {
185
185
  * @param encoded
186
186
  * @returns
187
187
  */
188
- singleTrack: (encoded: Base64) => Promise<TrackInfo>;
188
+ singleTrack: (encoded: Base64, requester: unknown) => Promise<Track>;
189
189
  /**
190
190
  *
191
191
  * @param encodeds Decodes multiple tracks into their info
192
192
  * @returns
193
193
  */
194
- multipleTracks: (encodeds: Base64[]) => Promise<TrackInfo[]>;
194
+ multipleTracks: (encodeds: Base64[], requester: unknown) => Promise<Track[]>;
195
195
  };
196
196
  /**
197
197
  * Request Lavalink statistics.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lavalink-client",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Easy and advanced lavalink client. Use it with lavalink plugins as well as latest lavalink versions",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",