Skip to content

Commit

Permalink
feat: lyrics API, add: fetchInfo when Node Connected `bypassChecks.no…
Browse files Browse the repository at this point in the history
…deFetchInfo` option to suppress the Error thrown
  • Loading branch information
UnschooledGamer committed Oct 24, 2024
1 parent 3e95d57 commit 84a70ab
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 14 deletions.
87 changes: 86 additions & 1 deletion build/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ export type RiffyOptions = {
* @description Default is false (only one track)
*/
multipleTrackHistory?: number | boolean;
/**
* @description Used to bypass few checks that throw Errors (Only Possible ones are listed below)
*/
bypassChecks: {
nodeFetchInfo: boolean;
}
} & Exclude<NodeOptions, "sessionId">

// In index.d.ts
Expand Down Expand Up @@ -462,6 +468,39 @@ type NodeInfoSemanticVersionObj = {
patch: number;
}

type LyricPluginWithoutLavaLyrics = "java-lyrics-plugin" | "lyrics"

/**
* @see https://github.com/DuncteBot/java-timed-lyrics
* @see https://github.com/DRSchlaubi/lyrics.kt
*/
export type LyricPluginWithoutLavaLyricsResult = {
type: "timed" | "text" | (string & {}),
track: {
title: string;
author: string;
album: string | null;
albumArt: {
url: string;
height: number;
width: number;
}[] | null;
};
source: string;
} | {
type: "text";
text: string;
} | {
type: "timed";
lines: {
line: string;
range: {
start: number;
end: number;
};
}[];
}

export interface NodeLyricsResult {
/** The name of the source */
sourceName: string;
Expand Down Expand Up @@ -504,6 +543,11 @@ export declare class Node {

public resumeKey: NodeOptions["resumeKey"];
public sessionId: NodeOptions["sessionId"];
/**
* Voice Regions Setup for the Node
* Helpful for region-based Node filtering.
* i.e If Voice Channel Region is `eu_west` Filter's Nodes specifically to `eu_west`
*/
public regions: string[] | null;
public resumeTimeout: NodeOptions["resumeTimeout"];
public autoResume: NodeOptions["autoResume"];
Expand All @@ -515,7 +559,10 @@ export declare class Node {

public connected: boolean;
public reconnecting: boolean;
public info: NodeInfo | {};
/**
* Lavalink Info fetched While/After connecting.
*/
public info: NodeInfo | null;
public stats: {
players: 0,
playingPlayers: 0,
Expand All @@ -537,6 +584,44 @@ export declare class Node {
deficit: 0,
},
};
public lastStats: number

/**
* fetches Lavalink Info
* returns null if some error occurred.
* @see https://lavalink.dev/api/rest.html#info
*/
fetchInfo(): Promise<NodeInfo | null>;

/**
* Lavalink Lyrics API (Works Only when Lavalink has Lyrics Plugin like: [lavalyrics](https://github.com/topi314/LavaLyrics))
*/
lyrics: {
/**
* Checks if the node has all the required plugins available.
* @param {boolean} [eitherOne=true] If set to true, will return true if at least one of the plugins is present.
* @param {...string} plugins The plugins to look for.
* @returns {Promise<boolean>} If the plugins are available.
* @throws {RangeError} If the plugins are missing.
*/
checkAvailable: (eitherOne: boolean, ...plugins: string[]) => Promise<boolean>;
/**
* Fetches lyrics for a given track or encoded track string.
*
* @param {Track|string} trackOrEncodedTrackStr - The track object or encoded track string.
* @param {boolean} [skipTrackSource=false] - Whether to skip the track source and fetch from the highest priority source (configured on Lavalink Server).
* @returns {Promise<Object|null>} The lyrics data or null if the plugin is unavailable Or If no lyrics were found OR some Http request error occured.
* @throws {TypeError} If `trackOrEncodedTrackStr` is not a `Track` or `string`.
*/
get: (trackOrEncodedTrackStr: Track | string, skipTrackSource: boolean) => Promise<NodeLyricsResult | null>;

/** @description fetches Lyrics for Currently playing Track
* @param {string} guildId The Guild Id of the Player
* @param {boolean} skipTrackSource skips the Track Source & fetches from highest priority source (configured on Lavalink Server)
* @param {string} [plugin] The Plugin to use(**Only required if you have too many known (i.e java-lyrics-plugin, lavalyrics-plugin) Lyric Plugins**)
*/
getCurrentTrack: <TPlugin extends LyricPluginWithoutLavaLyrics | (string & {})>(guildId: string, skipTrackSource: boolean, plugin?: TPlugin) => Promise<TPlugin extends LyricPluginWithoutLavaLyrics ? LyricPluginWithoutLavaLyricsResult : NodeLyricsResult | null>;
}

public connect(): void;
public open(): void;
Expand Down
120 changes: 109 additions & 11 deletions build/structures/Node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Websocket = require("ws");
const { Rest } = require("./Rest");
const { Track } = require("./Track");

class Node {
/**
Expand Down Expand Up @@ -28,8 +29,9 @@ class Node {
this.regions = node.regions;
/**
* Lavalink Info fetched While/After connecting.
* @type {import("..").NodeInfo | null}
*/
this.info = {};
this.info = null;
this.stats = {
players: 0,
playingPlayers: 0,
Expand Down Expand Up @@ -68,13 +70,80 @@ class Node {


lyrics = {
/** @description fetches Lyrics for Currently playing Track
* @param {boolean} skipTrackSource skips the Track Source & fetches from highest priority source (configured on Lavalink Server)
*/
getCurrentTrack: async (skipTrackSource) {

}
}
/**
* Checks if the node has all the required plugins available.
* @param {boolean} [eitherOne=true] If set to true, will return true if at least one of the plugins is present.
* @param {...string} plugins The plugins to look for.
* @returns {Promise<boolean>} If the plugins are available.
* @throws {RangeError} If the plugins are missing.
*/
checkAvailable: async (eitherOne=true,...plugins) => {
console.log("checkAvailable - plugins", ...plugins)
if (!this.sessionId) throw new Error(`Node (${this.name}) is not Ready/Connected.`)
if (!plugins.length) plugins = ["lavalyrics-plugin", "java-lyrics-plugin", "lyrics"];

const missingPlugins = [];

plugins.forEach((plugin) => {
const p = this.info.plugins.find((p) => p.name === plugin)

if (!p) {
missingPlugins.push(plugin)
return false;
}

return true;
});

if (!eitherOne && missingPlugins.length === plugins.length) {
throw new RangeError(`Node (${this.name}) is missing plugins: ${missingPlugins.join(", ")} (required for Lyrics)`)
} else if (missingPlugins.length) {
throw new RangeError(`Node (${this.name}) is missing plugins: ${missingPlugins.join(", ")} (required for Lyrics)`)
}

return true
},

/**
* Fetches lyrics for a given track or encoded track string.
*
* @param {Track|string} trackOrEncodedTrackStr - The track object or encoded track string.
* @param {boolean} [skipTrackSource=false] - Whether to skip the track source and fetch from the highest priority source (configured on Lavalink Server).
* @returns {Promise<Object|null>} The lyrics data or null if the plugin is unavailable Or If no lyrics were found OR some Http request error occured.
* @throws {TypeError} If `trackOrEncodedTrackStr` is not a `Track` or `string`.
*/
get: async (trackOrEncodedTrackStr, skipTrackSource=false) => {
if (!(await this.lyrics.checkAvailable(false, "lavalyrics-plugin"))) return null;
if(!(trackOrEncodedTrackStr instanceof Track) && typeof trackOrEncodedTrackStr !== "string") throw new TypeError(`Expected \`Track\` or \`string\` for \`trackOrEncodedTrackStr\` in "lyrics.get" but got \`${typeof trackOrEncodedTrackStr}\``)

let encodedTrackStr = typeof trackOrEncodedTrackStr === "string" ? trackOrEncodedTrackStr : trackOrEncodedTrackStr.track;

return await this.rest.makeRequest("GET",`/v4/lyrics?skipTrackSource=${skipTrackSource}&track=${encodedTrackStr}`);
},

/** @description fetches Lyrics for Currently playing Track
* @param {string} guildId The Guild Id of the Player
* @param {boolean} skipTrackSource skips the Track Source & fetches from highest priority source (configured on Lavalink Server)
* @param {string} [plugin] The Plugin to use(**Only required if you have too many known (i.e java-lyrics-plugin, lavalyrics-plugin) Lyric Plugins**)
*/
getCurrentTrack: async (guildId, skipTrackSource=false, plugin) => {
const DEFAULT_PLUGIN = "lavalyrics-plugin"
if (!(await this.lyrics.checkAvailable())) return null;

const nodePlugins = this.info?.plugins;
let requestURL = `/v4/sessions/${this.sessionId}/players/${guildId}/track/lyrics?skipTrackSource=${skipTrackSource}&plugin=${plugin}`

// If no `plugin` param is specified, check for `java-lyrics-plugin` or `lyrics` (also if lavalyrics-plugin is not available)
if(!plugin && (nodePlugins.find((p) => p.name === "java-lyrics-plugin") || nodePlugins.find((p) => p.name === "lyrics")) && !(nodePlugins.find((p) => p.name === DEFAULT_PLUGIN))) {
requestURL = `/v4/sessions/${this.sessionId}/players/${guildId}/lyrics?skipTrackSource=${skipTrackSource}`
} else if(plugin && ["java-lyrics-plugin", "lyrics"].includes(plugin)) {
// If `plugin` param is specified, And it's one of either `lyrics` or `java-lyrics-plugin`
requestURL = `/v4/sessions/${this.sessionId}/players/${guildId}/lyrics?skipTrackSource=${skipTrackSource}`
}

return await this.rest.makeRequest("GET", `${requestURL}`)
}
}

/**
* @typedef {Object} fetchInfoOptions
Expand Down Expand Up @@ -190,13 +259,17 @@ class Node {
this.ws.on("close", this.close.bind(this));
}

open() {
async open() {
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);

this.connected = true;
this.riffy.emit('debug', this.name, `Connection with Lavalink established on ${this.wsUrl}`);

/** @todo Add Version Checking of Node */
this.info = await this.fetchInfo().then((info) => this.info = info).catch((e) => (console.error(`Node (${this.name}) Failed to fetch info (${this.restVersion}/info) on WS-OPEN: ${e}`), null));

if (!this.info && !this.options.bypassChecks.nodeFetchInfo) {
throw new Error(`Node (${this.name} - URL: ${this.restUrl}) Failed to fetch info on WS-OPEN`);
}

if (this.autoResume) {
for (const player of this.riffy.players.values()) {
Expand Down Expand Up @@ -279,7 +352,32 @@ class Node {
}, this.reconnectTimeout);
}

destroy() {
/**
* Destroys the node connection and cleans up resources.
*
* @param {boolean} [clean=false] - Determines if a clean destroy should be performed.
* ### If `clean` is `true`
* it removes all listeners and nullifies the websocket,
* emits a "nodeDestroy" event, and deletes the node from the nodes map.
* ### If `clean` is `false`
* it performs the full disconnect process which includes:
* - Destroying all players associated with this node.
* - Closing the websocket connection.
* - Removing all listeners and nullifying the websocket.
* - Clearing any reconnect attempts.
* - Emitting a "nodeDestroy" event.
* - Deleting the node from the node map.
* - Setting the connected state to false.
*/
destroy(clean=false) {
if(clean) {
this.ws?.removeAllListeners();
this.ws = null;
this.riffy.emit("nodeDestroy", this);
this.riffy.nodes.delete(this.name);
return;
}

if (!this.connected) return;

this.riffy.players.forEach((player) => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
},
"homepage": "https://riffy.js.org/",
"devDependencies": {
"discord.js": "^14.15.3"
"discord.js": "^14.16.3"
}
}
4 changes: 3 additions & 1 deletion test/v4.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const { Client, GatewayDispatchEvents, AttachmentBuilder } = require("discord.js");
const { Riffy } = require("../build/index.js");
const { inspect } = require("node:util")

/**
* @type {import("discord.js").Client & { riffy: Riffy}}
*/
const client = new Client({
intents: [
"Guilds",
Expand Down

0 comments on commit 84a70ab

Please sign in to comment.