Regular plugin

So you are interested in using Rainlink and want to create your own plugin to extend some of Rainlink's features, so this page can help you somewhat.

Understanding how plugin works

Basically, the plugin in Rainlink will work by overriding the Rainlink class by adding or overriding variables and functions in it. Hence why it is called a plugin.

Requirements

  • Using rainlink package.
  • Extend RainlinkPlugin class.
  • Class named RainlinkPlugin (You can use import { RainlinkPlugin as Plugin } from "rainlink"; to avoid error).
  • Have name() function return the name of that plugin.
  • Have type() function return the type of that plugin using RainlinkPluginType enum. In this article, we will use default.
  • Have load() function to load all the modded function of that plugin.
  • Have unload() function to unload all the modded function of that plugin.

Do you know?

  • You can check all the requirement on src/Plugin/RainlinkPlugin.ts in official repo.
  • default type is different from sourceResolver. I will talk about sourceResolver later.

Example plugin code (YoutubeConverter / Not Release):

index.ts

import { RainlinkPlugin } from "./plugin";
export { RainlinkPlugin as DeezerPlugin }

plguin.ts

import { RainlinkTrack } from "rainlink";
import { RainlinkPluginType } from "rainlink";
import { RainlinkSearchOptions, RainlinkSearchResult, RainlinkSearchResultType } from "rainlink";
import { Rainlink } from "rainlink";
import { RainlinkPlugin as Plugin } from "rainlink";

const YOUTUBE_REGEX = [
  /^https?:\/\//,
  /(?:https?:\/\/)?(?:www\.)?youtu(?:\.be\/|be.com\/\S*(?:watch|embed)(?:(?:(?=\/[-a-zA-Z0-9_]{11,}(?!\S))\/)|(?:\S*v=|v\/)))([-a-zA-Z0-9_]{11,})/,
  /^.*(youtu.be\/|list=)([^#\&\?]*).*/,
];

export type YoutubeConvertOptions = {
  sources?: string[];
};

export class RainlinkPlugin extends Plugin {
  private options: YoutubeConvertOptions;
  private _search?: (query: string, options?: RainlinkSearchOptions) => Promise<RainlinkSearchResult>;
  constructor(options?: YoutubeConvertOptions) {
    super();
    this.options = options ?? { sources: ["scsearch"] };
    if (!this.options.sources || this.options.sources.length == 0) this.options.sources = ["scsearch"];
  }
  /** Name function for getting plugin name */
  public name(): string {
    return "rainlink-youtubeConvert";
  }

  /** Type function for diferent type of plugin */
  public type(): RainlinkPluginType {
    return RainlinkPluginType.Default;
  }

  /** Load function for make the plugin working */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public load(manager: Rainlink): void {
    this._search = manager.search.bind(manager);
    manager.search = this.search.bind(this);
  }

  /** unload function for make the plugin stop working */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  public unload(manager: Rainlink): void {
    if (!this._search) return;
    manager.search = this._search.bind(manager);
    this._search = undefined;
  }

  private async search(query: string, options?: RainlinkSearchOptions): Promise<RainlinkSearchResult> {
    // Check if search func avaliable
    if (!this._search) return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH);

    // Check if that's a yt link
    const match = YOUTUBE_REGEX.some((match) => {
      return match.test(query) == true;
    });
    if (!match) return await this._search(query, options);

    // Get search query
    const preRes = await this._search(query, options);
    if (preRes.tracks.length == 0) return preRes;

    // Remove track encoded to trick rainlink
    if (preRes.type == RainlinkSearchResultType.PLAYLIST) {
      for (const track of preRes.tracks) {
        track.encoded = "";
      }
      return preRes;
    }

    const song = preRes.tracks[0];
    const searchQuery = [song.author, song.title].filter((x) => !!x).join(" - ");
    const res = await this.searchEngine(searchQuery, options);
    if (res.tracks.length !== 0) return res;
    return preRes;
  }

  private async searchEngine(query: string, options?: RainlinkSearchOptions): Promise<RainlinkSearchResult> {
    if (!this._search) return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH);
    for (const SearchParams of this.options.sources!) {
      const res = await this._search(`directSearch=${SearchParams}:${query}`, options);
      if (res.tracks.length !== 0) return res;
    }
    return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH);
  }

  private buildSearch(
    playlistName?: string,
    tracks: RainlinkTrack[] = [],
    type?: RainlinkSearchResultType
  ): RainlinkSearchResult {
    return {
      playlistName,
      tracks,
      type: type ?? RainlinkSearchResultType.SEARCH,
    };
  }
}