Lexicon Plugins

Write your own plugins and share them with other Lexicon members

Lexicon lets you write plugins in Javascript with access to everything the Local API has to offer. You can run these plugins from inside the Lexicon GUI.

Please join Discord in the #developers channel to get help.

What is a plugin?

A plugin is a package with a config file that defines actions. The plugin actions are what the user runs. Each action has it's own set of permissions, settings and storage.

Plugin list

Plugins are loaded automatically when they are detected in the Documents/Lexicon/Plugins folder.

Everything a plugin does uses the Local API internally, so whatever you can do with the API you can also do with a plugin. A plugin just provides a GUI inside Lexicon and is more user friendly.

Plugin structure

A plugin is a ZIP file that contains:

  1. A config.json file with list of actions and permissions
  2. At least one Javascript action file
Plugin files

Configuration file

The config.json file defines all available actions and the permissions and settings for each action. It can look something like this (larger example in the example plugin):

{
  "id": "rekordcloud.example",
  "author": {
    "name": "Christiaan",
    "discordUsername": "Christiaan"
  },
  "actions": [
    {
      "id": "musicplayer.play",
      "name": "Play And Add Cues",
      "description": "This action will play the first selected track and add a cue every 4 beats after the first beatgrid marker.",
      "config": {
        "permissions": {
          "track": {
            "read": [
              "selected"
            ]
          },
          "control": true
        }
      }
    }
  ]
}

The following top level plugin properties are available:

{
  id: String // (required) Plugin identifier.
  author: { // (required) Author object.
    name: String // (required) Author (nick)name.
    discordUsername: String // (optional) Username on the Lexicon Discord.
    email: String // (optional) Contact email for Lexicon members. Discord username or email is required.
  }
  actions: Array[Action] // (required) Array of actions, see below.
}

Each action has the following properties:

{
  id: String // (required) Unique action ID and name of the Javascript file.
  name: String // (required) Action name visible to the user.
  description: String // (required) Action description.
  config: { // (required) Action configuration object.
    permissions: { // (required) Action permissions.
      track: { // (optional) Track permissions.
        read: Array[String]: Valid values: 'all', 'selected' // (optional) Read permissions. All tracks and/or selected tracks.
        modify: Array[String]: Valid values: 'all', 'selected' // (optional) Modify permissions. Must be either, cannot be both all & selected.
        modifyFields: Array[String]: Valid values see "Track Fields" schema in the Local API reference. // (required if track.modify) Fields that are going to be modified.
        delete: Boolean // (optional) Delete permission.
      }
      playlist: { // (optional) Playlist permissions
        read: Array[String]: Valid values: 'all', 'selected' // (optional) Read permissions. All playlists and/or selected playlists.
        modify: Array[String]: Valid values: 'all', 'selected' // (optional) Modify permissions. Must be either, cannot be both all & selected.
        modifyFields: Array[String]: Valid values: 'name', 'tracks', 'smartlist', 'parentId', 'position' // (optional) Fields that are going to be modified.
        create: Boolean // (optional) Create permission.
        delete: Boolean // (optional) Delete permission.
      }
      customTag: { // (optional) Custom Tag and Custom Tag category permissions.
        read: Array[String]: Valid values: 'all' // (optional) Read permissions.
        modify: Array[String]: Valid values: 'all' // (optional) Modify permissions. All fields can be modified. For available fields, see the Custom Tag schema in the Local API reference.
        create: Boolean // (optional) Create permission.
        delete: Boolean // (optional) Delete permission.
      }
      network: { // (optional) Network permissions
        GET: Array[String] // (optional) Array of whitelisted domains that the plugin can send a GET request to. For example: ['lexicondj.com']
        POST: Array[String] // (optiona) Same but for POST requests.
      }
      control: Boolean // (optional) Permission for the Control function. The control function has the same actions as the /control endpoint in the Local API, see API reference.
      storage: Boolean // (optional) Permission for this plugin action to store data in the Lexicon database, in its own database records.
    }
    settings: Object // (optional) Object with user settings that can be changed in the GUI. Set a value for a default setting. Example: { "Suffix": "Vocals" }
    allowParallel: Boolean // (optional) Allows this action to be run multiple times simultaneously.
    exclusive: Boolean // (optional) Enables a full screen loader and disables the Lexicon GUI. Use this for slow plugins that may break things when doing other work at the same time.
    confirmationMessage: String // (optional) If set, shows a warning message with confirmation before running this action.
  }
}

Example plugin

There is an offical example plugin that shows how to use most features of the plugin system. Highly recommend to look through it.

You can find the example plugin on Github.

Download the example plugin as ZIP file and put the ZIP file in the Documents/Lexicon/Plugins folder. Restart Lexicon and the Plugins menu will appear at the top.

If you want to edit and run the extracted plugin without ZIP file, read the Development chapter below.

Development

To make plugin development quicker and easier, there are a few development options. To use these, create the following JSON file: Documents/Lexicon/Plugins/development.json

This file has the following available properties:

{
  reloadBeforeRun: Boolean // (optional) Reloads the action javascript before running it.
  loadPluginFolders: Boolean // (optional) Loads plugins from folders, not just ZIP files. The folder must have the same contents as a plugin ZIP file.
}

A good starting point is to enable loadPluginFolders and clone the example plugin repo from Github into the Documents/Lexicon/Plugins folder.

Another tip to speed up development is to set a hotkey for your plugin action.

Distribution

To distribute a plugin, make a ZIP file of your script file(s) and config.json file. This ZIP file can be loaded by any Lexicon when it is put in the Documents/Lexicon/Plugins folder. A Lexicon restart is required the first time a plugin is added here.

For even easier distribution, you can link to the "Download ZIP" button on your Github repo for the plugin. This will simply ZIP the entire master branch and Lexicon will be able to read it.

If you want to share your plugin with other Lexicon members, please post it on our forum and don't hesitate to share or ask feedback on Discord #developers. ❤️

Caveats & limitations

The plugin Javascript is run in a protected environment so access to sensitive/dangerous functions is not possible. That means you can't use require() or import nor use the window or document object.

You should be able to use normal Javascript but because the script has a preprocessing stage with some limitations, there are a few things to keep in mind:

Don't use if-statement oneliners, but use curly brackets instead

❌ if (condition) throw new Error('Oops!')
✅ if (condition) {
  throw new Error('Oops!')
}

Don't use a do-while loop

❌ do {
  // Code
} while (condition)

Trailing semicolons after loops or brackets may in an "unexpected token" error

❌ if (condition) { 
  // Code
}; // <-- Remove this one
❌ for (let i = 0; i < 10; i++) {
  // Code
}; // <-- Remove this one

Explicitly set keys for your objects

❌ const index = 0
const obj = { index }
✅ const index = 0
const obj = { index: index }

Read / Update / Delete tracks

Creating new tracks is not yet possible.

Reading tracks

Reading tracks from the Lexicon library can be done in two ways:

  • Selected tracks (a user has selected tracks in the track browser). The "selected" permission is required in the config.permissions.track.read array.
  • All (non-archived) tracks. The "all" permission is required in the config.permissions.track.read array.

Selected tracks are available as array on the global variable _vars.tracksSelected. This is an array of all selected tracks that you can simply loop over.

All tracks is more complex because it's not possible to hold all tracks in memory, as some users may have up to a million tracks. To go through all tracks, you must call a function to get a next batch of tracks: await _library.track.getNextAllBatch(). Keep calling this function until the resulting array is of length 0. Then you've gone through all tracks. Keep in mind this does not include archived tracks. They are outside of the plugin scope. The total amount of (non-archived) tracks can be found in _vars.tracksAllAmount.

Without permission, the global variable and batch function will be null.

👉 See example plugin: tracks.count.js

Updating tracks

You can update any track object obtained from _vars.tracksSelected or _library.track.getNextAllBatch(). Just updating a field will track the modification. A field does need to be whitelisted to modify in the config.permissions.track.modifyFields array. If the field is missing, it will silently skip field.

All available fields and their types can be found in the Local API reference, see Schemas at the bottom.

You need the config.permissions.track.modify permission and whitelist the right fields in the config.permissions.track.modifyFields property.

👉 See example plugin: tracks.modify.js

Deleting tracks

You can delete tracks by calling the _library.track.delete with a track object as parameter. This will queue it for deletion when the plugin finishes.

Permission needs to be set in the config.permissions.track.delete property.

👉 See example plugin: tracks.delete.js

Create / Read / Update / Delete (CRUD) playlists

Reading playlists

Reading playlists from the Lexicon library can be done in two ways:

  • Selected playlists (a user has selected playlists in the playlist panel). The "selected" permission is required in the config.permissions.playlist.read array.
  • All playlists. The "all" permission is required in the config.permissions.playlist.read array.

You can access playlists with the _vars.playlistsSelected and _vars.playlistsAll variables. Tracks inside playlists can be obtained with playlist.getTrackIds() (faster but only track IDs) or playlist.getTracks() (slower but full track objects).

Without permission, the _vars.playlistsSelected and _vars.playlistsAll variables will be null.

Creating playlists & smartlists

You can create a playlist by calling the _library.playlist.create. Parameters are the same as the POST /playlist endpoint and can be found in the Local API reference.

👉 See example plugin: playlist.create.js & smartlist.create.js

Updating playlists

You can update a playlist by changing properties of the playlist objects inside the _vars.playlistsSelected and _vars.playlistsAll arrays.

You need the config.permissions.playlist.modify permission and whitelist the right fields in the config.permissions.playlist.modifyFields property.

👉 See example plugin: playlist.tracks.js

Deleting playlists

You can delete playlists by calling the _library.playlist.delete with a playlist object as parameter. This will queue it for deletion when the plugin finishes.

Permission needs to be set in the config.permissions.playlist.delete property.

👉 See example plugin: playlist.delete.js

Create / Read / Update / Delete (CRUD) Custom Tags & Custom Tag categories

Reading Custom Tags

Custom Tags and their categories are available in the _vars object:

  • _vars.customTags
  • _vars.customTagsCategories

Without config.permissions.customTag.read permission set to "all", these variables will be null.

👉 See example plugin: customtags.add.js

Creating Custom Tags

You can create a Custom Tags by calling the _library.customTag.create. Parameters are the same as the POST /tag endpoint and can be found in the Local API reference.

The same goes for creating a Custom Tag category with the _library.customTagCategory.create function with the POST /tag-category endpoint.

Permission needs to be set in the config.permissions.customTag.create property.

👉 See example plugin: customtags.add.js

Updating Custom Tags

You can update Custom Tags and categories by changing properties of the Custom Tag objects inside the _vars.customTags and _vars.customTagCategories arrays.

You need the config.permissions.customTag.modify permission.

👉 See example plugin: customtags.suffix.js

Deleting Custom Tags

You can delete Custom Tags and categories by calling the _library.customTag.delete with a Custom Tag object as parameter. This will queue it for deletion when the plugin finishes. The same goes for the _library.customTagCategory.delete function for Custom Tag categories.

Permission needs to be set in the config.permissions.customTag.delete property.

👉 See example plugin: customtags.delete.js

Settings & storage

Plugin support two kinds of ways to store items, settings and storage. Both is saved to the Lexicon database in a special record for the plugin action. Actions only have access to their own settings and storage.

Settings (for users)

Settings are intended for users to change. A plugin can only define the settings in the config.settings object with an optional default value. A user can change the settings. Settings persist in the Lexicon database.

Settings are always stored as string so if your type is something else like a number or array, you need to parse it.

Storage (for developers)

Storage is for internal plugin use and cannot be seen or changed by users. You can store any value here including objects and they will load as such.

Store a value by using the _storage.save("key", value) function.

Load a value by using the _storage.load("key") function.

👉 See example plugin: persistent.storage.js & tracks.search.js

Network requests

Plugins support making GET and POST requests. You need to whitelist domains you are going to make requests to.

You can whitelist any domain with the config.permissions.network.GET and config.permissions.network.POST arrays. Simply add the domain like so: ["lexicondj.com"]

You can then make a request with the helper functions: await _network.GET(params) and await _network.POST(params).

Parameter is an object like this:

{
  url: String // (required)
  headers: Object // (optional) Object of headers
  data: Object // (optional) Object of data added to the body of the request
}

These calls ignore any CORS restrictions and return the response body.

👉 See example plugin: network.get.js

Helpers

There are a few helper functions in the _helpers global:

_helpers.Log('message'): Log a message to the plugin log files at Documents/Plugins/Logs.
_helpers.Report('message'): Add a message as a new line to the report that is shown to the user when the action finishes.
await _helpers.Wait(1000) Asynchronous function that will wait the given amount of milliseconds.

Music Player

You can use the _musicplayer global to access functions that indicate what the music player is doing.

The following functions are available:

_musicplayer.getNowPlaying(): Returns the currently playing track object
_musicplayer.getQueue(): Returns the queued tracks as array of track objects
_musicplayer.getCurrentProgress(): Returns the current playing progress as number from 0 to 1
_musicplayer.getCurrentTime(): Returns the current playing progress as time in seconds
_musicplayer.getBpm(): Returns the current BPM

To control the music player, use the _ui.control() function, see below.

👉 See example plugin: musicplayer.read.js

User Input

You can pause your script and ask the user for input by using the asynchronous await _ui.showInputDialog() method. Make sure to use await. This function will return the inputted value as string or null if the popup was closed.

This function takes an object as argument with the follow properties:

{
  input: String // (optional) Type of input, can be "text" or "select". Defaults to "text".
  message: String // (required) Message to show the user
  options: Array[String] // (optional) For input=select only. Options to display to the user.
  default: String // (optional) Default value pre-filled in the input field
  settingsKey: String // (optional) Must correspond to a key in action config.settings. If supplied, this will save user input to that setting. This is the same setting that the user can edit by using the Lexicon GUI.
  type: Object // (optional) Type/color of popup. Can be: primary, success, info, warning, error
  confirmLabel: String // (optional) Label of the confirm button
  skipLabel: String // (optional) Label of the skip button
}
👉 See example plugin: ui.inputdialog.text.js & ui.inputdialog.select.js

User Interface

You can report progress to the user by using _ui.progress(0.5). This accepts a number as parameter between 0 and 1. This will be shown as percentage in the plugin loader.

Control

You can control Lexicon and much of what it can do with the control function: _ui.control(). A list of control actions can be found in the Local API reference under Schemas -> Control Actions at the bottom.

👉 See example plugin: musicplayer.play.js