yukumo
v1.7.0
Published
High-performance, framework-agnostic Lavalink v4 client for JavaScript and TypeScript
Downloads
1,008
Maintainers
Readme
YuKumo is a lightweight client library built to interface seamlessly with Lavalink v4 audio servers. It treats JavaScript (CommonJS & ESM) and TypeScript as equal first-class targets — JSDoc-powered autocomplete for JS consumers, and strict, fully-generic typing with zero any for TS projects.
Built for production: multi-node load balancing, automatic failover, distributed state via Redis, and OpenMetrics observability out of the box.
📖 Full documentation → yukumo.vercel.app
Table of Contents
Features
Core Protocol & Caching
- Full coverage of the Lavalink v4 REST API (search, decode, sessions, route planner, plugins) and WebSocket event dispatch
lavaSearchsupport for concurrent multi-category queries — tracks, albums, artists, playlists, and text sources- High-performance
SearchCacheLRU cache with configurable capacity (maxSize) and TTL support
Node Management
- 9 node-selection strategies:
RegionSelector,LeastUsed,LeastPenalty,CpuUsage,MemoryUsage,LowestPing,RoundRobin,Random, andCustomSelector - Zero-downtime automatic player migration on node disconnect or failure
- Built-in REST response caching with TTL, plus HTTP 429
Retry-Afterparsing and exponential backoff
Queueing & Player Controls
- Repeat modes (
off,track,queue), play history, shuffle, and priority track injection viapriorityEnqueue - Advanced queue helpers:
swap(),skipTo(),removeRange(), andclearExceptCurrent() - Smart Autoplay recommendation engine (
setAutoplay()) withautoplayTrackAddedevent notifications - Queue state serialization (
export()/import()) and pagination (getPage)
Audio & Filters
- Full DSP filter chain: Equalizer, Karaoke, Timescale, Tremolo, Vibrato, Rotation, Distortion, ChannelMix, LowPass
- High-level presets:
setBassBoost(),setNightcore(),setVaporwave(),setSlowedReverb(),set3DAudio(),setPitchShift(),setVoiceIsolation() - Global custom named filter preset registry (
FilterChain.registerPreset()/applyPreset())
Resilience & Protection new in 1.6
- WebSocket heartbeat with pong-timeout detection — half-open dead node connections are terminated and auto-reconnected
- Error-rate protection (
maxErrorsPerTime) destroys runaway players;minAutoPlayMsstops autoplay error spam queueEmptyDestroyMsauto-destroy timer after queue end, and standardizedDestroyReasonson everyplayerDestroyevent- Interpolated
player.positionbetween server updates, plusplayer.ping({ ws, lavalink })
Persistence new in 1.6
- Queue persistence (
queueOptions.persist): every queue mutation auto-saves to yourStorageAdapter(Memory/Redis) and restores after a restart - Full player state snapshots via
player.toJSON(); queue change hook viaqueue.onChanged
Voice State & Smart Behaviors
- 24/7 Mode (
stayInVc) to prevent channel disconnects on queue completion - Smart empty voice channel monitor (
setVcMemberCount()) with configurable auto-pause and auto-disconnect timeouts - First-class gateway adapters for
discord.jsv14,Eris,Seyfert,Oceanic.js,Davey, andDiscordeno
Lyrics, SponsorBlock & DX Utilities
- Server-side SponsorBlock plugin integration:
setSponsorBlock()categories withsegmentsLoaded/segmentSkipped/chapterStarted/chaptersLoadedevents - Live lyrics via the LavaLyrics plugin:
getCurrentLyrics(),subscribeLyrics()withlyricsLine/lyricsFound/lyricsNotFoundevents - Integrated LRCLIB synced lyrics (
getSyncedLyrics()) with timestamp parser (parseLrc()) - SponsorBlock segment skipping helper (
SponsorBlockClient) for skipping sponsor sections, intros, and outros - UI & Progress Bar helpers (
getProgressBar(),formatDuration(),createQueueEmbedData()) - Middleware interceptor registry (
MiddlewareRegistry/useBeforeTrackStart)
Control & Governance new in 1.6
- Rich play options:
play(track, { position, endTime, noReplace, paused, volume }) - Link policy:
linksAllowed,linksWhitelist,linksBlacklist(string or RegExp) gate URL queries - Custom HTTP headers per manager or per node; custom
Playersubclass viaplayerClass player.moveNode()least-loaded migration,queue.sortBy()/queue.removeTrack(),setAudioOutput("mono" | "left" | "right"),parseLavalinkConnUrl()
Plugins
- Pre-built wrappers for LavaSrc (Spotify, Apple Music, Deezer, Yandex Music), SponsorBlock segment filtering, and FloweryTTS
Observability & Logging
PrometheusExporterfor OpenMetrics-format output, ready for Grafana dashboards- Flexible logging via
ConsoleLogger,NoopLogger,levelFilteredLogger, or customLoggerimplementations - Drop-in
RedisStorageadapter for sharded and multi-process deployments
Installation
npm install yukumobun add yukumopnpm add yukumoQuick Start
const { Client, GatewayIntentBits } = require("discord.js");
const { YuKumo, DiscordJSAdapter, LeastPenaltySelector } = require("yukumo");
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
const yukumo = new YuKumo({
nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }],
defaultNodeSelector: new LeastPenaltySelector(),
});
const adapter = new DiscordJSAdapter(client, yukumo);
yukumo.on("nodeReady", (nodeId) => console.log(`[Yukumo] Node connected: ${nodeId}`));
yukumo.on("trackStart", (guildId, track) => console.log(`Now playing: ${track.info.title}`));
client.once("ready", async () => {
yukumo.setUserId(client.user.id);
await yukumo.init();
});
client.login(process.env.DISCORD_TOKEN);Usage Examples
client.on("messageCreate", async (message) => {
if (message.author.bot || !message.content.startsWith("!play ")) return;
const query = message.content.slice(6).trim();
const voiceChannel = message.member?.voice?.channel;
if (!voiceChannel) return message.reply("Join a voice channel first!");
const res = await yukumo.search(query);
if (res.tracks.length === 0) return message.reply("No tracks found!");
await yukumo.createPlayer({
guildId: message.guild.id,
voiceChannelId: voiceChannel.id,
textChannelId: message.channel.id,
});
adapter.sendVoiceStateUpdate(message.guild.id, voiceChannel.id);
await yukumo.play(message.guild.id, res.tracks[0]);
message.reply(`Playing: ${res.tracks[0].info.title}`);
});import { Client, GatewayIntentBits } from "discord.js";
import { YuKumo, DiscordJSAdapter, LeastUsedSelector } from "yukumo";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
const yukumo = new YuKumo({
nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }],
defaultNodeSelector: new LeastUsedSelector(),
});
const adapter = new DiscordJSAdapter(client, yukumo);
client.once("ready", async () => {
yukumo.setUserId(client.user.id);
await yukumo.init();
});
client.login(process.env.DISCORD_TOKEN);import { Client, GatewayIntentBits, Message } from "discord.js";
import { YuKumo, DiscordJSAdapter, TrackData, SearchResult, LeastPenaltySelector } from "yukumo";
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
const yukumo = new YuKumo({
nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }],
defaultNodeSelector: new LeastPenaltySelector(),
});
const adapter = new DiscordJSAdapter(client, yukumo);
client.once("ready", async () => {
if (!client.user) return;
yukumo.setUserId(client.user.id);
await yukumo.init();
});
client.on("messageCreate", async (message: Message) => {
if (message.author.bot || !message.guild || !message.member?.voice.channel) return;
if (!message.content.startsWith("!play ")) return;
const query = message.content.slice(6).trim();
const searchRes: SearchResult = await yukumo.search(query);
if (searchRes.tracks.length === 0) {
await message.reply("No tracks found.");
return;
}
const track: TrackData = searchRes.tracks[0];
const player = await yukumo.createPlayer({
guildId: message.guild.id,
voiceChannelId: message.member.voice.channel.id,
textChannelId: message.channel.id,
});
adapter.sendVoiceStateUpdate(message.guild.id, message.member.voice.channel.id);
await yukumo.play(message.guild.id, track);
player.filters.setBassBoost("medium");
await message.reply(`Now playing: ${track.info.title}`);
});
client.login(process.env.DISCORD_TOKEN);Plugins
| Plugin | Description | |---|---| | LavaSrc | Spotify, Apple Music, Deezer, and Yandex Music resolution | | SponsorBlock | Automatic segment filtering (intros, sponsors, outros) | | FloweryTTS | Text-to-speech track generation |
const { YuKumo, LavaSrcPlugin, SponsorBlockPlugin } = require("yukumo");
const yukumo = new YuKumo({
nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }],
plugins: [new LavaSrcPlugin(), new SponsorBlockPlugin()],
});Observability & Logging
Export live node and player metrics in OpenMetrics format for Prometheus / Grafana:
const { PrometheusExporter } = require("yukumo");
const exporter = new PrometheusExporter(yukumo);
exporter.listen(9090); // scrape at :9090/metricsConfigure custom loggers (ConsoleLogger, NoopLogger, or levelFilteredLogger) and LRU search caching:
const { YuKumo, ConsoleLogger, levelFilteredLogger, SearchCache } = require("yukumo");
const yukumo = new YuKumo({
nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }],
logger: levelFilteredLogger(new ConsoleLogger(), "info"),
searchCache: new SearchCache({ maxSize: 200, ttl: 1800000 }), // 30 min TTL
});Scale horizontally across processes with the built-in RedisStorage adapter.
Reference Bots
Complete, runnable bot implementations live in examples/:
| Bot | Description |
|---|---|
| examples/js-cjs/bot.js | CommonJS JavaScript |
| examples/js-esm/bot.js | ESM JavaScript |
| examples/ts/bot.ts | TypeScript |
Community & Contributing
- ⭐ Showcase — using YuKumo in production? Add your project to SHOWCASE.md
- 🤝 Contributing — see CONTRIBUTING.md for setup and guidelines
- 🛡️ Security — see SECURITY.md to report vulnerabilities
- 📖 Docs — yukumo.vercel.app
License
Distributed under the MIT License.
