Skip to content

Commit 0b2c441

Browse files
committed
v2.9.1 - new option to allow custom sources (unsupported sources...)
1 parent 5a37c19 commit 0b2c441

File tree

6 files changed

+135
-43
lines changed

6 files changed

+135
-43
lines changed

docs/src/content/docs/extra/version-log.mdx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ Logs about the changed features.
99

1010
---
1111

12+
## **Version 2.9.1**
13+
14+
- Introduce a `ManagerOption.playerOptions.allowCustomSources` Option, to allow to use "source" in the search query, which lavalink-client doesn't officially support yet (not in the `DEFAULT_SOURCES` object) This will allow you to make requests to sources, which lavalink-client doesn't support.
15+
16+
- Add JSDoc for all ManagerUtils.
17+
- New ManagerUtils + cleaner source extractions.
18+
1219
## **Version 2.9.0**
1320

1421
- Removed the autoChecks options on the manager:
15-
- `ManagerOption.autoChecks.pluginValidations` ... default is true, set to false if you want to skip plugin validations
16-
- `ManagerOption.autoChecks.sourcesValidations` ... default is true, set to false if you want to skip sources/filters validations
22+
- ~~`ManagerOption.autoChecks.pluginValidations`~~ -> Now configured per node: `NodeOption.autoChecks.pluginValidations`
23+
- ~~`ManagerOption.autoChecks.sourcesValidations`~~ -> Now configured per node: `NodeOption.autoChecks.sourcesValidations`
1724

1825
- Added autoChecks options for every single node:
1926
- `NodeOption.autoChecks.pluginValidations` ... default is true, set to false if you want to skip plugin validations (if nodeType === "NodeLink" then this will always be false)

docs/src/content/docs/home/configuration.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ client.lavalink = new LavalinkManager({
5959
applyVolumeAsFilter: false,
6060
clientBasedPositionUpdateInterval: 50, // in ms to up-calc player.position
6161
defaultSearchPlatform: "ytmsearch",
62+
allowCustomSources: false, // set to true if you want to allow custom sources which lavalink-client does not support (yet)
6263
volumeDecrementer: 0.75, // on client 100% == on lavalink 75%
6364
requesterTransformer: requesterTransformer,
6465
onDisconnect: {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lavalink-client",
3-
"version": "2.9.0",
3+
"version": "2.9.1",
44
"description": "Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients. - Supports NodeLink@v3 too.",
55
"keywords": [
66
"advanced",

src/structures/LavalinkManager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { DebugEvents, DestroyReasons } from "./Constants";
44
import { NodeManager } from "./NodeManager";
55
import { Player } from "./Player";
66
import { DefaultQueueStore } from "./Queue";
7+
import { ManagerUtils, MiniMap, safeStringify } from "./Utils";
8+
79
import type {
810
BotClientOptions,
911
DeepRequired,
@@ -14,7 +16,6 @@ import type {
1416
import type { LavalinkNodeOptions } from "./Types/Node";
1517
import type { PlayerOptions } from "./Types/Player";
1618
import type { ChannelDeletePacket, VoicePacket, VoiceServer, VoiceState } from "./Types/Utils";
17-
import { ManagerUtils, MiniMap, safeStringify } from "./Utils";
1819
export class LavalinkManager<CustomPlayerT extends Player = Player> extends EventEmitter {
1920
/**
2021
* Emit an event
@@ -113,6 +114,7 @@ export class LavalinkManager<CustomPlayerT extends Player = Player> extends Even
113114
applyVolumeAsFilter: options?.playerOptions?.applyVolumeAsFilter ?? false,
114115
clientBasedPositionUpdateInterval: options?.playerOptions?.clientBasedPositionUpdateInterval ?? 100,
115116
defaultSearchPlatform: options?.playerOptions?.defaultSearchPlatform ?? "ytsearch",
117+
allowCustomSources: options?.playerOptions?.allowCustomSources ?? false,
116118
onDisconnect: {
117119
destroyPlayer: options?.playerOptions?.onDisconnect?.destroyPlayer ?? true,
118120
autoReconnect: options?.playerOptions?.onDisconnect?.autoReconnect ?? false,
@@ -267,6 +269,7 @@ export class LavalinkManager<CustomPlayerT extends Player = Player> extends Even
267269
* applyVolumeAsFilter: false,
268270
* clientBasedPositionUpdateInterval: 150,
269271
* defaultSearchPlatform: "ytmsearch",
272+
* allowCustomSources: false,
270273
* volumeDecrementer: 0.75,
271274
* //requesterTransformer: YourRequesterTransformerFunction,
272275
* onDisconnect: {

src/structures/Types/Manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ export interface ManagerPlayerOptions<CustomPlayerT extends Player = Player> {
266266
clientBasedPositionUpdateInterval?: number;
267267
/** What should be used as a searchPlatform, if no source was provided during the query */
268268
defaultSearchPlatform?: SearchPlatform;
269+
/** Allow custom sources which lavalink-client does not support (yet) */
270+
allowCustomSources?: boolean;
269271
/** Applies the volume via a filter, not via the lavalink volume transformer */
270272
applyVolumeAsFilter?: boolean;
271273
/** Transforms the saved data of a requested user */

src/structures/Utils.ts

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { URL } from "node:url";
33
import { isRegExp } from "node:util/types";
44

55
import { DebugEvents } from "./Constants";
6-
import type { LavalinkManager } from "./LavalinkManager";
76
import { DefaultSources, LavalinkPlugins, SourceLinksRegexes } from "./LavalinkManagerStatics";
7+
8+
import type { LavalinkManager } from "./LavalinkManager";
89
import type { LavalinkNode } from "./Node";
910
import type { Player } from "./Player";
1011
import type { LavalinkNodeOptions } from "./Types/Node";
@@ -48,13 +49,25 @@ export class ManagerUtils {
4849
this.LavalinkManager = LavalinkManager;
4950
}
5051

52+
/**
53+
* Builds a pluginInfo object based on the provided data, extracting relevant information from the data and clientData parameters. This function is used to construct the pluginInfo property for tracks, allowing for consistent handling of plugin-related information across different track sources and formats.
54+
* @param data
55+
* @param clientData
56+
* @returns
57+
*/
5158
buildPluginInfo(data: any, clientData: any = {}) {
5259
return {
5360
clientData: clientData,
5461
...(data.pluginInfo || (data as any).plugin),
5562
};
5663
}
5764

65+
/**
66+
* Builds a Track object from the provided data and requester information. It validates the presence of required properties in the data, transforms the requester using a custom transformer function if provided, and constructs a Track object with the appropriate properties and plugin information. The function also includes error handling to ensure that the input data is valid and provides debug information if track building fails.
67+
* @param data
68+
* @param requester
69+
* @returns
70+
*/
5871
buildTrack(data: LavalinkTrack | Track, requester: unknown) {
5972
if (!data?.encoded || typeof data.encoded !== "string")
6073
throw new RangeError("Argument 'data.encoded' must be present.");
@@ -176,6 +189,11 @@ export class ManagerUtils {
176189
return true;
177190
}
178191

192+
/**
193+
* Gets the transformed requester based on the LavalinkManager options. If a custom requester transformer function is provided in the player options, it applies that function to the requester; otherwise, it returns the requester as is. The function also includes error handling to catch any exceptions that may occur during the transformation process and emits a debug event if the transformation fails.
194+
* @param requester
195+
* @returns
196+
*/
179197
getTransformedRequester(requester: unknown) {
180198
try {
181199
return typeof this.LavalinkManager?.options?.playerOptions?.requesterTransformer === "function"
@@ -298,6 +316,12 @@ export class ManagerUtils {
298316
return typeof data === "object" && !("info" in data) && typeof data.title === "string";
299317
}
300318

319+
/**
320+
* Gets the closest track by resolving the provided UnresolvedTrack using the getClosestTrack function. It includes error handling to catch any exceptions that may occur during the resolution process and emits a debug event if the resolution fails. The function returns a Promise that resolves to a Track object if successful, or undefined if no closest track is found.
321+
* @param data
322+
* @param player
323+
* @returns
324+
*/
301325
async getClosestTrack(data: UnresolvedTrack, player: Player): Promise<Track | undefined> {
302326
try {
303327
return getClosestTrack(data, player);
@@ -314,7 +338,14 @@ export class ManagerUtils {
314338
}
315339
}
316340

317-
validateQueryString(node: LavalinkNode, queryString: string, sourceString?: LavalinkSearchPlatform): void {
341+
/**
342+
* Validates the query string against various criteria, including checking for empty strings, length limits for specific sources, blacklisted links or words, and ensuring that the Lavalink node has the necessary source managers enabled for the provided query. The function also includes debug event emissions to assist with troubleshooting and understanding the validation process.
343+
* @param node
344+
* @param queryString
345+
* @param sourceString
346+
* @returns
347+
*/
348+
validateQueryString(node: LavalinkNode, queryString: string, sourceString?: SearchPlatform): void {
318349
if (!node.info) throw new Error("No Lavalink Node was provided");
319350
if (node._checkForSources && !node.info.sourceManagers?.length)
320351
throw new Error("Lavalink Node, has no sourceManagers enabled");
@@ -428,67 +459,115 @@ export class ManagerUtils {
428459
return;
429460
}
430461

431-
transformQuery(query: SearchQuery) {
432-
const sourceOfQuery =
433-
typeof query === "string"
434-
? undefined
435-
: (DefaultSources[
436-
query.source?.trim?.()?.toLowerCase?.() ??
437-
this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.()
438-
] ?? query.source?.trim?.()?.toLowerCase?.());
439-
const Query = {
440-
query: typeof query === "string" ? query : query.query,
441-
extraQueryUrlParams: typeof query !== "string" ? query.extraQueryUrlParams : undefined,
442-
source:
443-
sourceOfQuery ?? this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.(),
444-
};
462+
/**
463+
* Finds the source of a query string by checking if it starts with a valid source prefix defined in the DefaultSources object. If a valid source prefix is found, it returns the corresponding SearchPlatform; otherwise, it returns null. This function is useful for determining the intended search platform for a given query string, allowing for more accurate search results when the user specifies a source (e.g., "ytsearch:Never Gonna Give You Up" would indicate that the search should be performed on YouTube).
464+
* @param queryString
465+
* @returns
466+
*/
467+
findSourceOfQuery(queryString: string) {
445468
const foundSource = Object.keys(DefaultSources)
446-
.find((source) => Query.query?.toLowerCase?.()?.startsWith(`${source}:`.toLowerCase()))
469+
.find((source) => queryString?.toLowerCase?.()?.startsWith(`${source}:`.toLowerCase()))
447470
?.trim?.()
448471
?.toLowerCase?.() as SearchPlatform | undefined;
449472
// ignore links...
450473
if (foundSource && !["https", "http"].includes(foundSource) && DefaultSources[foundSource]) {
451-
Query.source = DefaultSources[foundSource]; // set the source to ytsearch:
452-
Query.query = Query.query.slice(`${foundSource}:`.length, Query.query.length); // remove ytsearch: from the query
474+
return foundSource;
453475
}
454-
return Query;
476+
return null;
477+
}
478+
479+
/**
480+
* Extracts the source from the query if it starts with a valid source prefix (e.g., "ytsearch:") and updates the searchQuery object accordingly.
481+
* @param searchQuery
482+
* @returns The updated searchQuery object with the extracted source and modified query string.
483+
*/
484+
extractSourceOfQuery<T extends { query: string; source?: string }>(searchQuery: T): T {
485+
const foundSource = this.findSourceOfQuery(searchQuery.query);
486+
// if query is like youtube:never gonna give you up, then this.findSourceOfQuery will return "youtube" as a valid source, since it's inside DEFAULT_SOURCES. Then it will be assigned as the proper source and removed from the query.
487+
if (foundSource) {
488+
searchQuery.source = DefaultSources[foundSource];
489+
searchQuery.query = searchQuery.query.slice(`${foundSource}:`.length, searchQuery.query.length); // remove ytsearch: from the query
490+
}
491+
return searchQuery;
455492
}
456493

494+
/**
495+
* Converts a string to lowercase if the input is a string, otherwise returns the input as is. This is useful for ensuring that search platform identifiers are case-insensitive while allowing other types of input to pass through unchanged.
496+
* @param input
497+
* @returns
498+
*/
499+
typedLowerCase<T extends unknown>(input: T) {
500+
if(!input) return input;
501+
if (typeof input === "string") return input.toLowerCase() as unknown as T;
502+
return input;
503+
}
504+
505+
/**
506+
* Transforms a search query by determining the appropriate search platform based on the query string and the default search platform specified in the LavalinkManager options. It checks if the query string starts with a valid source prefix and extracts it if present. The function returns an object containing the modified query string, any extra URL parameters, and the determined search platform to be used for the search operation.
507+
* @param query
508+
* @returns
509+
*/
510+
transformQuery(query: SearchQuery) {
511+
const typedDefault = this.typedLowerCase(this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform);
512+
if(typeof query === "string") {
513+
const Query = {
514+
query: query,
515+
extraQueryUrlParams: undefined,
516+
source: typedDefault,
517+
};
518+
return this.extractSourceOfQuery(Query);
519+
}
520+
const providedSource = query?.source?.trim?.()?.toLowerCase?.() as LavalinkSearchPlatform | undefined;
521+
const validSourceExtracted = DefaultSources[providedSource ?? typedDefault];
522+
return this.extractSourceOfQuery({
523+
query: query.query,
524+
extraQueryUrlParams: query.extraQueryUrlParams,
525+
source: validSourceExtracted ?? providedSource ?? typedDefault,
526+
});
527+
}
528+
529+
/**
530+
* Transforms a LavaSearchQuery by determining the appropriate search platform based on the query string and the default search platform specified in the LavalinkManager options. It checks if the query string starts with a valid source prefix and extracts it if present. The function returns an object containing the modified query string, any extra URL parameters, the determined search platform to be used for the search operation, and the types of search (track, playlist, artist, album, text) to be performed.
531+
* @param query
532+
* @returns
533+
*/
457534
transformLavaSearchQuery(query: LavaSearchQuery) {
535+
const typedDefault = this.typedLowerCase(this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform);
536+
if(typeof query === "string") {
537+
const Query = {
538+
query: query,
539+
extraQueryUrlParams: undefined,
540+
source: typedDefault,
541+
};
542+
return this.extractSourceOfQuery(Query);
543+
}
544+
const providedSource = query?.source?.trim?.()?.toLowerCase?.() as LavalinkSearchPlatform | undefined;
545+
const validSourceExtracted = DefaultSources[providedSource ?? typedDefault];
458546
// transform the query object
459-
const sourceOfQuery =
460-
typeof query === "string"
461-
? undefined
462-
: (DefaultSources[
463-
query.source?.trim?.()?.toLowerCase?.() ??
464-
this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.()
465-
] ?? query.source?.trim?.()?.toLowerCase?.());
466547
const Query = {
467-
query: typeof query === "string" ? query : query.query,
548+
query: query.query,
468549
types: query.types
469550
? ["track", "playlist", "artist", "album", "text"].filter((v) =>
470551
query.types?.find((x) => x.toLowerCase().startsWith(v)),
471552
)
472553
: ["track", "playlist", "artist", "album" /*"text"*/],
473554
source:
474-
sourceOfQuery ?? this.LavalinkManager?.options?.playerOptions?.defaultSearchPlatform?.toLowerCase?.(),
555+
validSourceExtracted ?? providedSource ?? typedDefault,
475556
};
476557

477-
const foundSource = Object.keys(DefaultSources)
478-
.find((source) => Query.query.toLowerCase().startsWith(`${source}:`.toLowerCase()))
479-
?.trim?.()
480-
?.toLowerCase?.() as SearchPlatform | undefined;
481-
if (foundSource && DefaultSources[foundSource]) {
482-
Query.source = DefaultSources[foundSource]; // set the source to ytsearch:
483-
Query.query = Query.query.slice(`${foundSource}:`.length, Query.query.length); // remove ytsearch: from the query
484-
}
485-
return Query;
558+
return this.extractSourceOfQuery(Query);
486559
}
487560

561+
/**
562+
* Validates the provided source string against the capabilities of the Lavalink node. It checks if the source string is supported by the node's enabled source managers and plugins, throwing errors if any required sources or plugins are missing for the specified search platform. This ensures that search queries are only executed with compatible sources based on the node's configuration.
563+
* @param node
564+
* @param sourceString
565+
* @returns
566+
*/
488567
validateSourceString(node: LavalinkNode, sourceString: SearchPlatform) {
489568
if (!sourceString) throw new Error(`No SourceString was provided`);
490569
const source = DefaultSources[sourceString.toLowerCase().trim()] as LavalinkSearchPlatform;
491-
if (!source) throw new Error(`Lavalink Node SearchQuerySource: '${sourceString}' is not available`);
570+
if (!source && !!this.LavalinkManager.options.playerOptions.allowCustomSources) throw new Error(`Lavalink-Client does not support SearchQuerySource: '${sourceString}'. You can disable this check by setting 'ManagerOptions.PlayerOptions.allowCustomSources' to true`);
492571

493572
if (!node.info) throw new Error("Lavalink Node does not have any info cached yet, not ready yet!");
494573

0 commit comments

Comments
 (0)