Plugin Development

This page covers the basics of writing a Pipe Bomb plugin. Separate pages cover each registerable component in detail.

Prerequisites

  • Node.js (the same version the server runs)
  • The Pipe Bomb server SDK (@pipe-bomb/plugin-sdk or the local sdk/ package from the server repo)

Plugin structure

A plugin is a Node.js package. The minimum required files:

my-plugin/
  package.json
  index.js        ← entrypoint (or whatever pipebombEntry/main points to)

package.json

{
	"name": "my-plugin",
	"version": "1.0.0",
	"main": "index.js",
	"pipebombEntry": "index.js"
}
  • name is the plugin's unique ID. It must match the id field in any marketplace manifest that lists it.
  • pipebombEntry takes precedence over main when the server loads the plugin. Use it if your build output is in a non-standard location.

If your plugin has a build script in package.json, the server will run npm run build automatically during installation before moving the plugin into the plugin directory.

Entrypoint

The entrypoint file must export a default class with enable and disable methods:

import type { Plugin, PluginApiContext } from "@pipe-bomb/plugin-sdk";

export default class MyPlugin implements Plugin {
	async enable(ctx: PluginApiContext): Promise<void> {
		// register capabilities here
	}

	disable(): void {
		// clean up if needed
	}
}

enable is called once when the server loads the plugin. disable is currently not called by the server (plugins are removed by deleting their directory and restarting).

PluginApiContext

ctx is your handle to the server. Everything your plugin registers or requests goes through it. Key methods:

Method Purpose
getLogger() Logger that prefixes output with your plugin name
getServerVersion() The running server version string
getPluginPackage() Your own package.json metadata
getPlugin(id) Get another loaded plugin's instance
requestTempDirectory() Allocate a temporary working directory
requestCacheDirectory() Allocate a persistent cache directory scoped to your plugin
getDataClient() Read tracks, albums, artists from the database
requestAuthClient() Generate and validate user JWTs
getPlaylistClient() Create and modify playlists
getWorkflowClient() Register workflow triggers and steps

Registration methods (registerLibraryHandler, registerAttributeSource, etc.) are covered on their respective pages.

Registerable components

Component Registration call Page
Library Handler registerLibraryHandler(handler) development/Library-Handler
Attribute Source registerAttributeSource(source) development/Attribute-Source
Track Identifier registerTrackIdentifier(id) users/Identifiers
Artist Identifier registerArtistIdentifier(id) users/Identifiers
Album Identifier registerAlbumIdentifier(id) users/Identifiers
Ephemeral Source registerEphemeralSource(source) users/Ephemeral-Sources
Search Source registerSearchSource(source) development/Search-Source
Task registerTask(task) See below
Global config registerConfigManager(mgr) development/Config-Manager
Per-user config registerUserConfigManager(id, mgr) development/Config-Manager
Workflow step/trigger getWorkflowClient() development/Workflow-Steps
Language strings registerLanguageDirectory(path)
Icons registerIconDirectory(path)
External URL source registerExternalUrlSource(source)

Tasks

A task is a background job users can trigger from Settings → Tasks or via a users/Workflows step. Register one with ctx.registerTask(task):

ctx.registerTask({
	id: "my-task",
	resumable: false,
	run: async (ctx) => {
		ctx.update(0);
		// ... do work ...
		ctx.update(1);
	},
});

For tasks with sub-variants (e.g., "all" vs "new"), use getSubTasks():

ctx.registerTask({
	id: "my-task",
	resumable: true,
	getSubTasks: () => ["all", "new"] as const,
	run: async (ctx, subTaskId) => {
		// subTaskId is "all" or "new"
	},
});

resumable: true means the server stores progress by run ID so an interrupted task can be detected and restarted at the correct offset.