Plugin Development
Extend YuKumo with lifecycle hooks and custom modules.
Extend YuKumo with lifecycle hooks and custom modules.
Plugin Interface
interface Plugin {
name: string; // must be unique
version: string;
init?(): void | Promise<void>;
start?(): void | Promise<void>;
destroy?(): void | Promise<void>;
}Lifecycle
| Phase | When | Purpose |
|---|---|---|
init() | On register | Setup, connect services, add hooks |
start() | YuKumo.init() | Begin after nodes connect |
destroy() | Unregister / shutdown | Cleanup, remove hooks |
Basic plugin
import type { Plugin, PluginManager } from "yukumo";
export class LoggingPlugin implements Plugin {
readonly name = "logging";
readonly version = "1.0.0";
private manager?: PluginManager;
init(manager?: PluginManager): void {
this.manager = manager;
}
start(): void {
this.manager?.addHook("beforePlay", async (_guildId, track) => {
console.log(`Playing: ${track.info.title}`);
return track;
});
}
destroy(): void {
console.log("Logging plugin stopped");
}
}
// Usage
new YuKumo({
nodes: [...],
plugins: [new LoggingPlugin()],
});Lifecycle Hooks
Add a hook
client.plugins.addHook("beforeSearch", async (query, source) => {
return { query, source: source ?? "ytsearch" };
});Cancel an operation
Return null:
client.plugins.addHook("beforeSearch", async (query) => {
if (query.trim() === "") return null;
return { query, source: undefined };
});Available hooks
| Hook | Use Case |
|---|---|
beforeSearch | Modify or validate queries |
afterSearch | Filter or transform results |
beforeConnect | Validate voice channel join |
afterConnect | Log connection events |
beforePlay | Modify or block tracks |
afterPlay | Log or update status |
beforeDestroy | Prevent destruction |
afterDestroy | Clean up resources |
onNodeSelect | Override node selection |
Execution order
- Hooks run in registration order
- Each receives the previous hook's output
- Returning
nullcancels the pipeline onNodeSelect: first non-null result wins
Real-world examples
Track blacklist
export class TrackBlacklistPlugin {
readonly name = "track-blacklist";
readonly version = "1.0.0";
private blacklist = new Set<string>();
constructor(ids: string[]) {
ids.forEach((id) => this.blacklist.add(id));
}
init(manager: PluginManager): void {
manager.addHook("beforePlay", async (_guildId, track) => {
if (this.blacklist.has(track.info.identifier)) {
console.log(`Blocked: ${track.info.title}`);
return null;
}
return track;
});
}
addToBlacklist(id: string): void {
this.blacklist.add(id);
}
}Custom node selector
client.plugins.addHook("onNodeSelect", async (guildId, availableNodes) => {
if (premiumGuilds.has(guildId) && availableNodes.includes("premium")) {
return "premium";
}
return null;
});Analytics
export class AnalyticsPlugin implements Plugin {
readonly name = "analytics";
readonly version = "1.0.0";
private plays = 0;
init(manager: PluginManager): void {
manager.addHook("afterPlay", async () => {
this.plays++;
});
}
destroy(): void {
console.log(`Total plays: ${this.plays}`);
}
}Best Practices
- Keep hooks fast — they run synchronously in the pipeline
- Handle errors — wrap in try/catch to avoid breaking the pipeline
- Unique names — duplicate registration throws
PluginError - Clean up in
destroy()— remove hooks to prevent memory leaks
export class CleanPlugin implements Plugin {
readonly name = "clean";
readonly version = "1.0.0";
private manager?: PluginManager;
private handler = async (guildId: string) => {
console.log(`Destroyed player in ${guildId}`);
};
init(manager: PluginManager): void {
this.manager = manager;
manager.addHook("afterDestroy", this.handler);
}
destroy(): void {
this.manager?.removeHook("afterDestroy", this.handler);
}
}