npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dreama-link

v1.0.2

Published

A powerful but simple, Lavalink v4 client for Discord.js bots! supports ESM & CJS, full REST API, filters, queues, session resuming, DAVE E2EE, and plugin support.

Readme

dreama-link

A powerful but simple, Lavalink v4 client library for Discord.js bots.
Supports ESM and CommonJS, session resuming, multi-node load balancing, a fluent filter builder, a full REST API wrapper, a built-in track queue, DAVE E2EE, and auto-detected Lavalink plugins.


Table of Contents


Features

  • 🔗 Full Lavalink v4 protocol support (WebSocket + REST)
  • 📦 ESM and CommonJS dual-package support
  • 🚀 Discord.js v14 and v15 compatible (supports both ready and clientReady)
  • ✨ Complete REST API wrapper with typed responses
  • 🔌 Powerful FilterBuilder — nightcore, vaporwave, bass boost, and full manual control
  • 🔍 Built-in Queue with loop modes (track, queue, none), shuffle, history, and more
  • 🤹‍♂️ Multi-node support with automatic load balancing via penalty scoring
  • ‼️ Session resuming — music keeps playing through bot restarts
  • 🟢 Automatic reconnection with configurable exponential backoff
  • 💪 Strongly typed with full TypeScript definitions
  • 🛡️ DAVE E2EE protocol support (Discord Audio & Video End-to-End Encryption)
  • 🔧 Auto-detected plugins with colored console output on node connect
  • 🎛️ SponsorBlock — skip segments, load chapters, full REST and event support
  • 🎤 LavaLyrics — fetch real-time lyrics for any track
  • 🎵 LavaSrc — Spotify, Apple Music, Deezer, Yandex Music, JioSaavn search
  • 🔎 LavaSearch — multi-source search in a single call
  • 🚪 Auto-disconnectleaveOnEmpty and leaveOnEnd built in

Requirements

  • Node.js >= 18.0.0
  • A running Lavalink v4 server
  • discord.js >= 14.0.0 (peer dependency, v15 also supported)

Installation

npm install dreama-link
# or
yarn add dreama-link
# or
pnpm add dreama-link

Quick Start

import { Client, GatewayIntentBits, Events } from 'discord.js';
import { Manager } from 'dreama-link';

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildVoiceStates,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

const manager = new Manager({
  nodes: [
    {
      host: 'localhost',
      port: 2333,
      password: 'youshallnotpass',
      secure: false,
      name: 'main',
      retryAmount: 5,
      retryDelay: 3000,
      resumeTimeout: 60,
    },
  ],
  autoPlay: true,
  defaultVolume: 100,
  defaultSelfDeaf: true,
  leaveOnEmpty: true,
  leaveOnEmptyDelay: 30000,
  leaveOnEnd: false,
  send(guildId, payload) {
    const guild = client.guilds.cache.get(guildId);
    guild?.shard.send(payload);
  },
});

// Works with both discord.js v14 and v15
client.once(Events.ClientReady, (c) => {
  manager.init(c.user.id);
  console.log(`Logged in as ${c.user.tag}`);
});

client.on('raw', (data) => {
  if (data.t === 'VOICE_SERVER_UPDATE') manager.handleVoiceServerUpdate(data.d);
  if (data.t === 'VOICE_STATE_UPDATE') manager.handleVoiceStateUpdate(data.d);
});

manager.on('nodeReady', (node) => {
  console.log(`Node ${node.name} is ready`);
});

manager.on('trackStart', (player, track) => {
  console.log(`Now playing: ${track.info.title} by ${track.info.author}`);
});

manager.on('queueEnd', (player) => {
  console.log(`Queue ended in guild ${player.guildId}`);
});

manager.on('channelEmpty', (player) => {
  console.log(`Voice channel empty in guild ${player.guildId} — leaving...`);
});

client.on('messageCreate', async (message) => {
  if (!message.guild || message.author.bot) return;
  if (!message.content.startsWith('!')) return;

  const args = message.content.slice(1).trim().split(/ +/g);
  const command = args.shift()?.toLowerCase();

  if (command === 'play') {
    const voiceChannel = message.member?.voice.channel;
    if (!voiceChannel) return message.reply('Join a voice channel first!');

    let player = manager.getPlayer(message.guild.id);
    if (!player) {
      player = manager.createPlayer({
        guildId: message.guild.id,
        voiceChannelId: voiceChannel.id,
        textChannelId: message.channel.id,
      });
      await player.connect();
    }

    const query = args.join(' ');
    const result = await manager.search({ source: 'youtube', query });

    if (result.loadType === 'error') return message.reply('Failed to load track.');
    if (result.loadType === 'empty') return message.reply('No results found.');

    if (result.loadType === 'playlist') {
      player.queue.add(result.data.tracks);
      message.reply(`Added playlist **${result.data.info.name}** (${result.data.tracks.length} tracks)`);
    } else {
      const track = result.loadType === 'search' ? result.data[0] : result.data;
      player.queue.add(track);
      message.reply(`Added **${track.info.title}** to the queue`);
    }

    if (!player.playing) await player.play();
  }
});

client.login('YOUR_BOT_TOKEN');

Core Concepts

Manager

The Manager is the central hub. It manages all nodes, players, voice state routing, and optional features like DAVE and auto-disconnect. You create one Manager per bot process and call manager.init(clientId) once the Discord client is ready.

[!WARNING] If there is no clientId given, voice states, players, and nodes won't be initialized.

Node

A Node represents a connection to a single Lavalink server. The Manager maintains a pool of nodes and automatically routes players to the least-loaded one. On connect, each node's installed plugins are auto-detected and printed to the console with color.

Player

A Player controls audio playback for a single Discord guild. It holds a Queue, current playback state, filters, and voice connection info. Create players via manager.createPlayer(). Calling player.connect() before player.play() is required — play() now waits automatically for the voice connection to be fully established with Lavalink before sending the play request, fixing the issue where audio metadata was received but nothing actually played.

Queue

Each player has a built-in Queue that manages the list of upcoming tracks with three loop modes, shuffle, history navigation, and range operations.

FilterBuilder

A fluent builder for constructing Lavalink audio filters. Includes preset helpers like nightcore(), vaporwave(), and bassBoost().

RestClient

A fully typed HTTP client that wraps every Lavalink v4 REST endpoint including plugin endpoints. Accessible via node.rest.


API Reference

Manager API

Constructor

new Manager(options: ManagerOptions)

| Option | Type | Default | Description | |---|---|---|---| | nodes | NodeOptions[] | required | Array of Lavalink node configurations | | clientId | string | '' | Discord bot user ID (can also be set via init()) | | clientName | string | 'dreama-link' | Identifier sent in the Client-Name header | | shards | number | 1 | Number of shards your bot uses | | autoPlay | boolean | true | Automatically play next track on track end | | defaultVolume | number | 100 | Default player volume (0–1000) | | defaultSelfDeaf | boolean | true | Whether the bot deafens itself by default when joining VC | | defaultSelfMute | boolean | false | Whether the bot mutes itself by default when joining VC | | defaultLoop | LoopMode | 'none' | Default loop mode for all new players | | leaveOnEmpty | boolean | false | Automatically destroy player when voice channel becomes empty | | leaveOnEmptyDelay | number | 30000 | Milliseconds to wait before leaving an empty channel | | leaveOnEnd | boolean | false | Automatically destroy player when the queue ends | | leaveOnEndDelay | number | 0 | Milliseconds to wait before leaving after queue ends | | dave | boolean | false | Enable Discord DAVE E2EE protocol support | | send | (guildId, payload) => void | required | Function that sends a gateway payload to Discord |

Methods

manager.init(clientId: string): Manager

Initializes the manager with the bot's user ID and connects all configured nodes.

manager.addNode(options: NodeOptions): Node
manager.removeNode(name: string): boolean
manager.getNode(name?: string): Node
manager.getBestNode(): Node
manager.createPlayer(options: PlayerOptions): Player
manager.getPlayer(guildId: string): Player | undefined
manager.destroyPlayer(guildId: string): Promise<void>
manager.search(query: string | SearchQuery, requester?: unknown): Promise<LoadResult>

Searches for tracks. Pass sources array in SearchQuery to trigger LavaSearch multi-source mode.

manager.handleVoiceServerUpdate(data: VoiceServerUpdate): void
manager.handleVoiceStateUpdate(data: VoiceStateUpdate): void

Must be called from your Discord raw event handler.

manager.handleDAVEPacket(guildId: string, packet: DAVERawPacket): void

Forward raw DAVE voice gateway packets for a specific guild. Requires dave: true.

manager.getDAVEState(guildId: string): DAVEState | null

Returns the current DAVE state for a guild.

manager.sendVoiceUpdate(guildId: string, channelId: string | null, options?): void

Properties

| Property | Type | Description | |---|---|---| | nodes | Map<string, Node> | All registered nodes | | players | Map<string, Player> | All active players | | connectedNodes | Node[] | Nodes that are currently connected | | totalPlayers | number | Total number of active players | | playingPlayers | number | Number of players currently playing |


Node API

NodeOptions

| Option | Type | Default | Description | |---|---|---|---| | host | string | required | Lavalink server host | | port | number | required | Lavalink server port | | password | string | required | Lavalink server password | | secure | boolean | false | Use WSS/HTTPS | | name | string | host:port | Unique identifier for this node | | retryAmount | number | 5 | Max reconnection attempts | | retryDelay | number | 3000 | Base delay between retries (ms) | | resumeKey | string | '' | Enables session resuming when set | | resumeTimeout | number | 60 | Session resume timeout in seconds | | requestTimeout | number | 10000 | REST request timeout (ms) |

Properties

| Property | Type | Description | |---|---|---| | name | string | Node identifier | | state | NodeState | 'disconnected' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'destroyed' | | connected | boolean | Shorthand for state === 'connected' | | sessionId | string \| null | Current Lavalink session ID | | stats | NodeStats \| null | Latest stats from Lavalink | | info | LavalinkInfo \| null | Node info fetched on connect | | rest | RestClient | REST API client for this node | | activePlugins | Set<string> | Plugin names detected on this node | | penalties | number | Computed load score used for balancing |

Methods

node.connect(): void
node.destroy(): void
node.send(data: unknown): void
node.hasPlugin(name: string): boolean

Player API

PlayerOptions

| Option | Type | Default | Description | |---|---|---|---| | guildId | string | required | Discord guild ID | | voiceChannelId | string | required | Voice channel to join | | textChannelId | string | — | Text channel for responses | | selfDeaf | boolean | defaultSelfDeaf | Whether the bot deafens itself | | selfMute | boolean | defaultSelfMute | Whether the bot mutes itself | | volume | number | defaultVolume | Initial volume | | nodeName | string | — | Pin to a specific node by name |

Playback Methods

player.connect(): Promise<void>
player.disconnect(): Promise<void>
player.play(track?: Track, options?: PlayOptions): Promise<void>
player.pause(): Promise<void>
player.resume(): Promise<void>
player.stop(): Promise<void>
player.seek(position: number): Promise<void>
player.setVolume(volume: number): Promise<void>
player.skip(): Promise<Track | null>
player.previous(): Promise<Track | null>
player.destroy(disconnect?: boolean): Promise<void>

[!NOTE] play() automatically waits for the voice connection to be fully established with Lavalink before sending the play request. You no longer need to manually delay or poll player.connected.

Filter Methods

player.setFilters(filters: Filters): Promise<void>
player.applyFilterBuilder(): Promise<void>
player.clearFilters(): Promise<void>

SponsorBlock Methods

Requires the sponsorblock-plugin to be installed on your Lavalink node.

player.setSponsorBlockCategories(categories: SponsorBlockCategory[]): Promise<void>
player.getSponsorBlockCategories(): Promise<SponsorBlockCategory[]>
player.clearSponsorBlock(): Promise<void>

LavaLyrics Methods

Requires the lavalyrics-plugin to be installed on your Lavalink node.

player.getLyrics(skipTrackSource?: boolean): Promise<LyricsData | null>
player.getLyricsByTrack(track: Track, skipTrackSource?: boolean): Promise<LyricsData | null>

Voice Readiness Methods

player.waitForVoice(timeoutMs?: number): Promise<void>
player.setVoiceReady(): void

Properties

| Property | Type | Description | |---|---|---| | guildId | string | Guild ID | | voiceChannelId | string | Voice channel ID | | textChannelId | string \| undefined | Text channel ID | | queue | Queue | The player's queue | | playing | boolean | Whether a track is playing | | paused | boolean | Whether playback is paused | | volume | number | Current volume | | state | PlayerState | Latest player state from Lavalink | | filters | Filters | Currently applied filters | | filterBuilder | FilterBuilder | Filter builder instance | | loop | LoopMode | Current loop mode | | position | number | Current playback position in ms | | ping | number | Latency to Discord voice server | | connected | boolean | Whether the bot is in a voice channel | | node | Node | The node this player is attached to |


Queue API

Methods

queue.add(track: Track | Track[], position?: number): void
queue.remove(index: number): Track | undefined
queue.removeRange(start: number, end: number): Track[]
queue.move(from: number, to: number): boolean
queue.shuffle(): void
queue.clear(): void
queue.clearHistory(): void
queue.next(): Track | null
queue.previous(): Track | null
queue.peek(): Track | null
queue.toArray(): Track[]
queue.at(index: number): Track | undefined

Properties

| Property | Type | Description | |---|---|---| | current | Track \| null | Currently playing track | | loop | LoopMode | 'none' \| 'track' \| 'queue' | | size | number | Number of tracks in the queue | | isEmpty | boolean | Whether the queue is empty | | totalDuration | number | Total duration of queued tracks in ms | | history | Track[] | Previously played tracks (last 50) |


FilterBuilder API

const filters = new FilterBuilder()
  .setVolume(1.0)
  .setBand(0, 0.3)
  .setTimescale({ speed: 1.2, pitch: 1.2 })
  .setRotation({ rotationHz: 0.2 })
  .build();

await player.setFilters(filters);

Preset Methods

builder.nightcore(): this
builder.vaporwave(): this
builder.bassBoost(level?: number): this

Manual Methods

builder.setVolume(volume: number): this
builder.setEqualizer(bands: EqualizerBand[]): this
builder.setBand(band: number, gain: number): this
builder.setKaraoke(options: KaraokeFilter): this
builder.setTimescale(options: TimescaleFilter): this
builder.setTremolo(options: TremoloFilter): this
builder.setVibrato(options: VibratoFilter): this
builder.setRotation(options: RotationFilter): this
builder.setDistortion(options: DistortionFilter): this
builder.setChannelMix(options: ChannelMixFilter): this
builder.setLowPass(options: LowPassFilter): this
builder.setPluginFilter(pluginName: string, config: object): this

Clear Methods

builder.clearEqualizer(): this
builder.clearTimescale(): this
builder.clearAll(): this
builder.build(): Filters
FilterBuilder.from(filters: Filters): FilterBuilder

RestClient API

Accessible via node.rest. All methods return typed Promises.

Track API

rest.loadTracks(identifier: string): Promise<LoadResult>
rest.decodeTrack(encodedTrack: string): Promise<Track>
rest.decodeTracks(encodedTracks: string[]): Promise<Track[]>

Player API

rest.getPlayers(sessionId: string): Promise<LavalinkPlayer[]>
rest.getPlayer(sessionId: string, guildId: string): Promise<LavalinkPlayer>
rest.updatePlayer(sessionId, guildId, options, noReplace?): Promise<LavalinkPlayer>
rest.destroyPlayer(sessionId: string, guildId: string): Promise<void>

Session API

rest.updateSession(sessionId: string, options: UpdateSessionOptions): Promise<SessionInfo>

Server Info

rest.getInfo(): Promise<LavalinkInfo>
rest.getVersion(): Promise<string>
rest.getStats(): Promise<NodeStats>

RoutePlanner API

rest.getRoutePlannerStatus(): Promise<RoutePlannerStatus | null>
rest.freeRoutePlannerAddress(address: string): Promise<void>
rest.freeAllRoutePlannerAddresses(): Promise<void>

SponsorBlock API

rest.getSponsorBlockCategories(sessionId, guildId): Promise<SponsorBlockCategory[]>
rest.setSponsorBlockCategories(sessionId, guildId, categories): Promise<void>
rest.deleteSponsorBlockCategories(sessionId, guildId): Promise<void>

LavaLyrics API

rest.getLyrics(sessionId, guildId, skipTrackSource?): Promise<LyricsData | null>
rest.getLyricsByTrack(encodedTrack, skipTrackSource?): Promise<LyricsData | null>

Events

Manager Events

| Event | Arguments | Description | |---|---|---| | nodeReady | node, resumed | A node connected successfully | | nodeDisconnect | node, code, reason | A node disconnected | | nodeReconnecting | node | A node is attempting to reconnect | | nodeError | node, error | A node encountered an error | | nodeStats | node, stats | Node stats updated (~every 1 min) | | nodeDestroy | node | A node was destroyed | | trackStart | player, track | A track started playing | | trackEnd | player, track, reason | A track ended | | trackError | player, track, exception | A track threw an exception | | trackStuck | player, track, thresholdMs | A track got stuck | | queueEnd | player | Queue ran out of tracks | | playerCreate | player | A player was created | | playerDestroy | player | A player was destroyed | | playerUpdate | player, state | Player state updated | | playerPause | player | Player was paused | | playerResume | player | Player was resumed | | playerVolumeChange | player, oldVolume, newVolume | Player volume changed | | voiceDisconnect | player, code, reason | Discord voice WS closed | | socketClosed | player, code, reason, byRemote | Discord voice WS closed (with byRemote flag) | | channelEmpty | player | Voice channel became empty (fires before auto-leave) | | segmentsLoaded | player, segments | SponsorBlock segments loaded for track | | segmentSkipped | player, segment | SponsorBlock segment was auto-skipped | | chaptersLoaded | player, chapters | SponsorBlock chapters loaded for track | | chapterStarted | player, chapter | SponsorBlock chapter started | | lyricsLine | player, line | LavaLyrics line event (real-time sync) | | lyricsFound | player, lyrics | LavaLyrics found lyrics for current track | | lyricsNotFound | player | LavaLyrics could not find lyrics | | daveReady | player, guildId | DAVE E2EE is ready for a guild | | daveTransitionFinished | player, guildId, transitionId | DAVE MLS transition completed | | daveError | guildId, error | DAVE protocol error |

Node Events

| Event | Arguments | Description | |---|---|---| | ready | node, resumed | Node connected | | disconnect | node, code, reason | Node disconnected | | reconnecting | node | Node is reconnecting | | error | node, error | Node error | | stats | node, stats | Stats received | | destroy | node | Node destroyed | | raw | node, message | Raw WebSocket message received |

Player Events

| Event | Arguments | Description | |---|---|---| | trackStart | player, track | Track started | | trackEnd | player, track, reason | Track ended | | trackError | player, track, exception | Track error | | trackStuck | player, track, thresholdMs | Track stuck | | queueEnd | player | Queue empty | | playerUpdate | player, state | State update | | playerPause | player | Player paused | | playerResume | player | Player resumed | | playerVolumeChange | player, oldVolume, newVolume | Volume changed | | voiceDisconnect | player, code, reason | Voice WS closed | | socketClosed | player, code, reason, byRemote | Voice WS closed with byRemote | | segmentsLoaded | player, segments | SponsorBlock segments loaded | | segmentSkipped | player, segment | SponsorBlock segment skipped | | chaptersLoaded | player, chapters | SponsorBlock chapters loaded | | chapterStarted | player, chapter | SponsorBlock chapter started | | lyricsLine | player, line | LavaLyrics real-time line | | lyricsFound | player, lyrics | LavaLyrics lyrics found | | lyricsNotFound | player | LavaLyrics no lyrics found | | destroy | player | Player destroyed |


Plugin Support

dreama-link auto-detects Lavalink plugins when a node connects by reading /v4/info. Detected plugins are printed to the console with color — green for supported plugins, yellow for unknown ones.

SponsorBlock

Plugin: sponsorblock-plugin v3.0.1+

Enables automatic segment skipping and chapter detection for YouTube tracks.

await player.setSponsorBlockCategories(['sponsor', 'selfpromo', 'interaction']);

manager.on('segmentSkipped', (player, segment) => {
  console.log(`Skipped ${segment.category} from ${segment.start}s to ${segment.end}s`);
});

manager.on('chapterStarted', (player, chapter) => {
  console.log(`Chapter: ${chapter.name}`);
});

manager.on('segmentsLoaded', (player, segments) => {
  console.log(`Loaded ${segments.length} segments`);
});

SponsorBlock categories: sponsor · selfpromo · interaction · intro · outro · preview · music_offtopic · filler

LavaLyrics

Plugin: lavalyrics-plugin v1.1.0+

Fetch synchronized or plain lyrics for any track.

const lyrics = await player.getLyrics();
if (lyrics) {
  console.log(lyrics.text);
}

const lyricsByTrack = await player.getLyricsByTrack(track);

manager.on('lyricsLine', (player, line) => {
  console.log(`[${line.range.start}ms] ${line.line}`);
});

manager.on('lyricsFound', (player, lyrics) => {
  console.log(`Lyrics found from: ${lyrics.sourceName}`);
});

LavaSrc

Plugin: lavasrc-plugin v4.4.0+

Unlocks Spotify, Apple Music, Deezer, Yandex Music, and JioSaavn as search sources.

const result = await manager.search({ source: 'spotify', query: 'Blinding Lights' });
const result2 = await manager.search({ source: 'applemusic', query: 'Bohemian Rhapsody' });
const result3 = await manager.search({ source: 'deezer', query: 'bad guy Billie Eilish' });

LavaSearch

Plugin: lavasearch-plugin v1.0.0+

Search multiple sources simultaneously by passing a sources array.

const result = await manager.search({
  query: 'Never Gonna Give You Up',
  sources: ['youtube', 'spotify', 'deezer'],
});

When sources has more than one entry, dreama-link automatically uses the mzsearch: prefix required by LavaSearch.

YouTube Source

Plugin: youtube-source v1.18.0+

Replaces the built-in YouTube source with an improved version supporting age-restricted videos and more.

const result = await manager.search({ source: 'youtube', query: 'your query' });
const result2 = await manager.search({ source: 'youtubemusic', query: 'your query' });

DAVE E2EE Protocol

Discord's DAVE (Discord Audio & Video End-to-End Encryption) is an opt-in E2EE layer for voice using the MLS (Message Layer Security) protocol. Enable it in your manager options and forward DAVE voice gateway packets:

const manager = new Manager({
  dave: true,
  // ...
});

client.ws.on('raw', (payload) => {
  if (payload.t === 'VOICE_SERVER_UPDATE') manager.handleVoiceServerUpdate(payload.d);
  if (payload.t === 'VOICE_STATE_UPDATE') manager.handleVoiceStateUpdate(payload.d);
});

manager.on('daveReady', (player, guildId) => {
  console.log(`DAVE E2EE is active in guild ${guildId}`);
});

manager.on('daveTransitionFinished', (player, guildId, transitionId) => {
  console.log(`DAVE MLS transition ${transitionId} complete in guild ${guildId}`);
});

manager.on('daveError', (guildId, error) => {
  console.error(`DAVE error in guild ${guildId}:`, error.message);
});

const state = manager.getDAVEState(guildId);
console.log(state?.ready, state?.protocolVersion);

Forward raw voice gateway DAVE packets (opcodes 21–27) using:

manager.handleDAVEPacket(guildId, { op: 26, d: {} });

Ready Event Compatibility

dreama-link works with all Discord.js ready event patterns. Call manager.init(client.user.id) inside your ready handler regardless of which event syntax you use:

// discord.js v14 — classic
client.once('ready', (c) => {
  manager.init(c.user.id);
});

// discord.js v14 — Events enum (recommended)
client.once(Events.ClientReady, (c) => {
  manager.init(c.user.id);
});

// discord.js v15 — clientReady (new name, ready is deprecated)
client.once('clientReady', (c) => {
  manager.init(c.user.id);
});

// discord.js v15 — Events enum (forward-compatible)
client.once(Events.ClientReady, (c) => {
  manager.init(c.user.id);
});

Using Events.ClientReady from the discord.js package is the most forward-compatible approach and works across both v14 and v15.


Types Reference

type LoopMode = 'none' | 'track' | 'queue';
type TrackEndReason = 'finished' | 'loadFailed' | 'stopped' | 'replaced' | 'cleanup';
type ExceptionSeverity = 'common' | 'suspicious' | 'fault';
type NodeState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'destroyed';
type LoadResultType = 'track' | 'playlist' | 'search' | 'empty' | 'error';
type SponsorBlockCategory = 'sponsor' | 'selfpromo' | 'interaction' | 'intro' | 'outro' | 'preview' | 'music_offtopic' | 'filler';
type SearchSource = 'youtube' | 'youtubemusic' | 'soundcloud' | 'spotify' | 'applemusic' | 'deezer' | 'yandexmusic' | 'jiosaavn' | 'mzsearch' | string;

Key interfaces: Track, TrackInfo, Filters, PlayerState, NodeStats, LavalinkInfo, LoadResult, LavalinkException, LyricsData, SponsorBlockSegment, SponsorBlockChapter, DAVEState.

All types are re-exported from the main package entry.


ESM & CJS Usage

ESM (TypeScript / modern Node)

import { Manager, FilterBuilder, DAVEProtocol } from 'dreama-link';

CommonJS

const { Manager, FilterBuilder, DAVEProtocol } = require('dreama-link');

Advanced Usage

Session Resuming

{
  host: 'localhost',
  port: 2333,
  password: 'youshallnotpass',
  resumeKey: 'my-bot-resume-key',
  resumeTimeout: 60,
}

Multi-Node Setup

const manager = new Manager({
  nodes: [
    { host: 'node1.example.com', port: 2333, password: 'pass', name: 'us-east' },
    { host: 'node2.example.com', port: 2333, password: 'pass', name: 'eu-west', secure: true },
  ],
  send(guildId, payload) { /* ... */ },
});

manager.createPlayer({ guildId, voiceChannelId, nodeName: 'us-east' });

Auto-Disconnect

const manager = new Manager({
  leaveOnEmpty: true,
  leaveOnEmptyDelay: 30000,
  leaveOnEnd: true,
  leaveOnEndDelay: 5000,
  // ...
});

manager.on('channelEmpty', (player) => {
  sendMessage(player.textChannelId, 'Channel is empty — leaving in 30 seconds...');
});

Filters

await player.filterBuilder
  .nightcore()
  .applyFilterBuilder();

await player.filterBuilder
  .clearAll()
  .bassBoost(0.4)
  .setRotation({ rotationHz: 0.2 })
  .applyFilterBuilder();

Checking Active Plugins

const node = manager.getBestNode();

if (node.hasPlugin('sponsorblock')) {
  await player.setSponsorBlockCategories(['sponsor', 'intro', 'outro']);
}

if (node.hasPlugin('lavalyrics')) {
  const lyrics = await player.getLyrics();
}

console.log([...node.activePlugins]);

Search Sources

| Source Key | Prefix | Plugin Required | Description | |---|---|---|---| | youtube | ytsearch: | built-in / yt-source | YouTube search | | youtubemusic | ytmsearch: | built-in / yt-source | YouTube Music search | | soundcloud | scsearch: | built-in | SoundCloud search | | spotify | spsearch: | LavaSrc | Spotify search | | applemusic | amsearch: | LavaSrc | Apple Music search | | deezer | dzsearch: | LavaSrc | Deezer search | | yandexmusic | ymsearch: | LavaSrc | Yandex Music search | | jiosaavn | jssearch: | LavaSrc | JioSaavn search | | sources[] | mzsearch: | LavaSearch | Multi-source search |

Pass a direct URL to skip the prefix entirely:

manager.search({ query: 'https://open.spotify.com/track/...' });

License

MIT — see LICENSE for details.

Contributions are open! Create a Pull Request anytime you want to.