hibiki.js
v1.0.3
Published
A powerful, fully-featured Lavalink / NodeLink client for Discord bots. First-class TypeScript support, built-in resolvers, smart search, fairplay queuing, voice receive, and more.
Downloads
20
Maintainers
Readme
Hibiki
A powerful Lavalink & NodeLink client for Discord bots. Built for both beginners who want something that just works and advanced users who need full control.
npm install hibiki.jsQuick Start
import { Hibiki } from "hibiki.js";
import { Client, GatewayIntentBits } from "discord.js";
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
const manager = new Hibiki({
nodes: [{ host: "localhost", port: 2333, password: "youshallnotpass" }],
sendPayload: (guildId, payload) => {
client.guilds.cache.get(guildId)?.shard.send(payload);
},
defaultSearchEngine: "ytmsearch",
});
client.once("ready", () => {
manager.init(client.user!.id);
console.log("Ready!");
});
client.on("raw", (packet) => {
if (packet.t === "VOICE_STATE_UPDATE" || packet.t === "VOICE_SERVER_UPDATE") {
manager.voice.handle(packet);
}
});
manager.on("trackStart", (player, track) => {
console.log(`Now playing: ${track.info.title}`);
});
client.login("your-token");Playing Music
const player = manager.createPlayer({
guildId: "123456789",
voiceChannelId: "987654321",
textChannelId: "111111111",
selfDeaf: true,
});
player.connect();
// Search and play
const result = await player.search("Never Gonna Give You Up");
if (result.tracks.length) {
player.queue.add(result.tracks[0]);
await player.play();
}
// Smart search — finds the best match across engines
const smart = await player.smartSearch("blinding lights weeknd");
if (smart.bestMatch) {
player.queue.add(smart.bestMatch);
await player.play();
}Spotify / Apple Music / Deezer
Hibiki ships built-in resolvers that convert Spotify, Apple Music and Deezer tracks into playable audio using your configured search engines — no separate plugin needed.
import { Spotify, Deezer, Apple } from "hibiki.js";
const spotify = new Spotify({
clientId: "your-spotify-client-id",
clientSecret: "your-spotify-client-secret",
engines: ["ytmsearch", "spsearch"], // tried in order
});
const deezer = new Deezer();
const apple = new Apple();
// Single track
const track = spotify.url("https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT");
player.queue.add(track); // resolves automatically when it's about to play
// Playlist (UnresolvedTrack[] — resolved lazily as they play)
const playlist = await spotify.playlist("https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M");
player.queue.add(playlist);
await player.play();
// Deezer playlist
const deezerPlaylist = await deezer.playlist("https://www.deezer.com/playlist/1234567");
player.queue.add(deezerPlaylist.tracks);
// From title + artist (no API call needed, resolves at play time)
const unresolved = spotify.query("Blinding Lights", "The Weeknd", msg.author);
player.queue.add(unresolved);Tracks are resolved lazily — only when they're about to play — so adding a 500-track playlist is instant.
Queue
// Add tracks
player.add(track); // preferred — handles fairplay + emits queueUpdate
player.queue.add(track); // raw add
player.queue.addNext(track); // play after current
player.queue.prepend(track); // insert at position 1
// Inspect
player.queue.current; // currently playing (may be unresolved)
player.queue.next; // next up
player.queue.upcoming; // everything after current
player.queue.previous; // history (always resolved, newest first)
player.queue.size; // total including current
player.queue.duration(); // ms, excludes streams
// Manipulate
player.queue.remove(2); // remove index 2
player.queue.move(3, 1); // move index 3 to index 1
player.queue.shuffle();
player.queue.clear();
player.queue.clearPast(); // clear history
// Bump a track (or range) to top
await player.bump(5); // move position 5 to top
await player.bump(3, 6); // move positions 3–6 to top
// Loop
player.repeat("none"); // "none" | "track" | "queue"
// Fairplay — interleaves tracks so no single user dominates the queue
player.setFairplay(true);
player.fairplaySort();
player.fairplayStats(); // { total, requesters, distribution }
// Listen for any queue change
manager.on("queueUpdate", (player, reason, queue) => {
// reason: "add" | "remove" | "move" | "shuffle" | "clear" | "fairplay" | "bump"
updateNowPlayingMessage(player);
});Player Controls
await player.pause();
await player.resume();
await player.skip();
await player.previous(); // go back in history
await player.stop(); // stop + clear queue
await player.seek(30000); // seek to 30s
await player.forward(10000); // forward 10s
await player.rewind(10000); // rewind 10s
await player.replay(); // restart current track
await player.vol(80); // 0–1000
player.grab(); // get current track
// 24/7 mode
player.set24(true);
// Autoplay
player.setAutoplay(true);
// Move to a different node
await player.moveNode("node-2");Filters
import { FilterBuilder } from "hibiki.js";
await player.filter(new FilterBuilder().bassBoost(2));
await player.filter(new FilterBuilder().nightcore());
await player.filter(new FilterBuilder().vaporwave());
await player.filter(new FilterBuilder().eightD());
await player.filter(new FilterBuilder().soft());
await player.filter(new FilterBuilder().trebleBass());
await player.filter(new FilterBuilder().television());
// Custom
await player.filter(
new FilterBuilder()
.setTimescale({ speed: 1.1, pitch: 1.05 })
.setEqualizer([{ band: 0, gain: 0.3 }, { band: 1, gain: 0.2 }])
);
await player.unfilter();NodeLink Extras
// Lyrics — real-time line-by-line
await player.lyricsOn();
manager.on("lyricsLine", (player, line) => console.log(line.line));
manager.on("lyricsNotFound", (player) => console.log("No lyrics"));
// Or fetch full lyrics at once
const lyrics = await player.lyrics();
// YouTube chapters
const chapters = await player.chapters();
// Mix layers — play a second track over the current one
const mix = await player.mix(encodedTrack, 0.5); // volume 0–1
await player.unmix(mix.mixId);
// Fading
await player.fading({
enabled: true,
trackStart: { duration: 2000, curve: "linear" },
trackEnd: { duration: 3000, curve: "exponential" },
});
// Voice receive
const socket = manager.voiceReceive(guildId);
socket.on("packet", (packet) => { /* raw PCM */ });
// YouTube live chat
const chat = manager.liveChat({ videoId: "dQw4w9WgXcQ" });
chat.on("message", (msg) => console.log(msg));Events
manager.on("nodeReady", (node) => {});
manager.on("nodeDisconnect", (node, code, reason) => {});
manager.on("nodeError", (node, error) => {});
manager.on("nodeStats", (node, stats) => {});
manager.on("trackStart", (player, track) => {});
manager.on("trackEnd", (player, track, reason) => {});
manager.on("trackError", (player, track, exception) => {});
manager.on("trackStuck", (player, track, threshold) => {});
manager.on("queueEnd", (player) => {});
manager.on("queueUpdate", (player, reason, queue) => {});
manager.on("playerCreate", (player) => {});
manager.on("playerDestroy", (player) => {});
manager.on("playerMove", (player, oldChannel, newChannel) => {});
manager.on("playerDisconnect", (player, channelId) => {});
manager.on("playerPause", (player) => {});
manager.on("playerResume", (player) => {});
manager.on("playerSeek", (player, oldPos, newPos) => {});
manager.on("volumeChange", (player, oldVol, newVol) => {});
manager.on("nodeSwitched", (player, oldNode, newNode) => {});
manager.on("socketClosed", (player, code, reason, byRemote) => {});
manager.on("fairplayToggled", (player, enabled) => {});
// Custom autoplay handler
manager.on("autoplay", async (player) => {
const result = await player.search("chill lofi music");
if (result.tracks[0]) {
player.queue.add(result.tracks[0]);
await player.play();
}
});
manager.on("debug", (msg) => console.log("[Hibiki]", msg));Error Handling
import { PlayerError, ErrorCode } from "hibiki.js";
try {
await player.seek(99999);
} catch (e) {
if (e instanceof PlayerError) {
switch (e.code) {
case ErrorCode.PLAYER_SEEK_STREAM:
return msg.reply("You can't seek a live stream.");
case ErrorCode.PLAYER_NOT_PLAYING:
return msg.reply("Nothing is playing.");
case ErrorCode.PLAYER_DESTROYING:
return;
default:
console.error(e);
}
}
}Available codes: NOT_INITIATED, ALREADY_INITIATED, NO_NODES, NODE_NOT_FOUND, NODE_ALREADY_EXISTS, NODE_NOT_CONNECTED, PLAYER_DESTROYING, PLAYER_NOT_PLAYING, PLAYER_EMPTY_QUEUE, PLAYER_NO_HISTORY, PLAYER_SEEK_STREAM, PLAYER_SEEK_UNRESOLVED, TRACK_RESOLVE_FAILED, REST_RATE_LIMITED, REST_REQUEST_FAILED, NODELINK_ONLY.
Persistence
import type { PersistenceAdapter, SavedState } from "hibiki.js";
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
const adapter: PersistenceAdapter = {
async save(guildId, state) {
await redis.set(`hibiki:${guildId}`, JSON.stringify(state), { EX: 86400 });
},
async load(guildId) {
const raw = await redis.get(`hibiki:${guildId}`);
return raw ? JSON.parse(raw) : null;
},
async delete(guildId) {
await redis.del(`hibiki:${guildId}`);
},
async keys() {
const keys = await redis.keys("hibiki:*");
return keys.map((k) => k.replace("hibiki:", ""));
},
};
const manager = new Hibiki({ persistence: adapter, ...rest });
// Restore players after restart
const voiceMap = new Map([
["guild123", { voiceChannelId: "vc456", textChannelId: "txt789" }],
]);
const restored = await manager.restoreAllPlayers(voiceMap);
console.log(`Restored ${restored} players`);Plugins
import type { Plugin } from "hibiki.js";
const myPlugin: Plugin = {
name: "my-plugin",
load(manager) {
console.log("Plugin loaded!");
},
unload(manager) {
console.log("Plugin unloaded.");
},
onTrackStart(player, track) {
console.log(`[${player.guildId}] Started: ${track.info.title}`);
},
onQueueEnd(player) {},
onNodeDisconnect(node, code, reason) {
console.warn(`Node ${node.name} disconnected: ${code} ${reason}`);
},
};
const manager = new Hibiki({ plugins: [myPlugin], ...rest });
manager.loadPlugin(myPlugin);
manager.unloadPlugin("my-plugin");Shard-Aware Node Routing
const manager = new Hibiki({
totalShards: 4,
nodes: [
{ name: "node-shards-0-1", host: "node1.example.com", port: 2333, password: "pass", shards: [0, 1] },
{ name: "node-shards-2-3", host: "node2.example.com", port: 2333, password: "pass", shards: [2, 3] },
],
...rest,
});
manager.createPlayer({ guildId, voiceChannelId, shardId: 2 });Custom Queue
import { Queue } from "hibiki.js";
import type { QueueEntry } from "hibiki.js";
class MyQueue extends Queue {
add(track: QueueEntry | QueueEntry[], position?: number): void {
super.add(track, position);
}
}
const manager = new Hibiki({ queueConstructor: MyQueue, ...rest });Requirements
- Node.js 18+
- A running Lavalink v4 or NodeLink v3 server
