Skip to content

Quick Setup

The RyanlinkManager is the central class in Ryanlink. It manages Lavalink nodes, creates and destroys audio players, and handles Discord voice state updates. It extends Node’s EventEmitter.

  1. Create the manager

    import { Client, GatewayIntentBits } from 'discord.js';
    import { RyanlinkManager } from 'ryanlink';
    const client = new Client({
    intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
    });
    const manager = new RyanlinkManager({
    nodes: [{
    id: 'main',
    host: 'localhost',
    port: 2333,
    authorization: 'youshallnotpass',
    autoChecks: { pluginValidations: false, sourcesValidations: false },
    }],
    sendToShard: (guildId, payload) => {
    const totalShards = client.ws.shards.size || 1;
    const shardId = Number((BigInt(guildId) >> 22n) % BigInt(totalShards));
    const shard = client.ws.shards.get(shardId);
    if (shard) shard.send(payload);
    else client.guilds.cache.get(guildId)?.shard?.send(payload);
    },
    client: { id: process.env.CLIENT_ID! },
    resuming: { enabled: true, timeout: 60000 },
    });
  2. Forward voice events

    client.on('raw', (packet) => {
    if (manager.initiated) {
    manager.provideVoiceUpdate(packet).catch(() => {});
    }
    });
  3. Initialize on ready

    client.once('ready', async () => {
    await manager.init({ id: client.user!.id, username: client.user!.username });
    console.log('Ryanlink ready');
    });
  4. Play a track

    // In your slash command handler
    const player = manager.createPlayer({
    guildId: interaction.guildId!,
    voiceChannelId: interaction.member.voice.channelId!,
    textChannelId: interaction.channelId,
    selfDeaf: true,
    });
    if (!player.connected) await player.connect();
    const result = await manager.search(
    { query: 'never gonna give you up', source: 'ytsearch' },
    interaction.user
    );
    if (!result.tracks.length) {
    return interaction.reply('No results found.');
    }
    player.queue.add(result.tracks[0]);
    if (!player.playing) await player.play();

new RyanlinkManager(options: RyanConfiguration)

Required options:

  • nodes — array of node configurations (can be empty [])
  • sendToShard — function to send gateway payloads to Discord
  • client.id — your bot’s user ID

| Property | Type | Description | | :--- | :--- | :--- | | options | RyanConfiguration | Applied configuration with defaults | | nodeManager | NodeManager | Manages all Lavalink nodes | | utils | RyanlinkUtils | Internal utility functions | | players | MiniMap<string, Player> | Active players keyed by guildId | | voiceStates | Map<string, VoiceStateEntry> | Cached Discord voice states | | initiated | boolean | Whether init() has been called | | useable | boolean | true if at least one node is connected |

Connects to all configured Lavalink nodes. Call once after the bot is ready.

await manager.init({ id: client.user.id, username: client.user.username });

Creates a new player for a guild, or returns the existing one.

const player = manager.createPlayer({ guildId: '...', voiceChannelId: '...' });

Returns the player for a guild, or undefined.

const player = manager.getPlayer('123456789');

Destroys a player — disconnects from voice, cleans up resources, emits playerDestroy. This is the correct way to stop a player.

await manager.destroyPlayer('123456789', 'User left');

Removes a player from the internal map without full cleanup. Use destroyPlayer instead unless you know what you’re doing. Throws if the player is still connected and dontThrowError is not set.

manager.search(query, requestUser?, node?, throwOnEmpty?)

Section titled “manager.search(query, requestUser?, node?, throwOnEmpty?)”

Searches for tracks on the least-used connected node (or a specific node).

const result = await manager.search({ query: 'daft punk', source: 'spsearch' }, user);
// result.loadType — 'search' | 'track' | 'playlist' | 'empty' | 'error'
// result.tracks — Track[]
// result.playlist — PlaylistInfo | null

Handles raw Discord gateway payloads. Forward all VOICE_STATE_UPDATE, VOICE_SERVER_UPDATE, and CHANNEL_DELETE events here.

client.on('raw', (packet) => manager.provideVoiceUpdate(packet));

manager.getVoiceStateUsers(guildId, channelId)

Section titled “manager.getVoiceStateUsers(guildId, channelId)”

Returns cached voice state entries for users in a specific channel.

const users = manager.getVoiceStateUsers('guildId', 'channelId');

Returns { initiated, playerCount, nodeCount }.


// Play (uses queue.current or shifts from queue.tracks)
await player.play();
// Pause / resume
await player.pause(true); // pause
await player.pause(false); // resume
await player.resume(); // same as pause(false), throws if not paused
// Seek (ms)
await player.seek(30000);
// Skip
await player.skip(); // skip current track
await player.skip(2); // skip 2 tracks ahead
// Stop
await player.stopPlaying(clearQueue: true, executeAutoplay: false);
// Volume (0–1000)
await player.setVolume(80);
// Repeat
await player.setRepeatMode('off' | 'track' | 'queue');
await player.connect();
await player.disconnect();
await player.setVoiceChannel({ voiceChannelId: 'newChannelId' });
player.set('myKey', value);
player.get<T>('myKey');
player.setData('myKey', value);
player.getData<T>('myKey');
player.deleteData('myKey');
player.clearData();
player.getAllData();

| Property | Type | Description | | :--- | :--- | :--- | | guildId | string | Guild ID | | voiceChannelId | string \| null | Current voice channel | | textChannelId | string \| null | Text channel for messages | | playing | boolean | Whether audio is playing | | paused | boolean | Whether audio is paused | | volume | number | Current volume (0–1000) | | internalVolume | number | Volume after volumeDecrementer | | position | number | Current playback position (ms, computed) | | repeatMode | 'off' \| 'track' \| 'queue' | Repeat mode | | autoplay | boolean | Whether autoplay is enabled | | queue | Queue | The player’s queue | | filterManager | FilterManager | Filter controls | | node | RyanlinkNode \| NodeLinkNode | The assigned audio node | | ping | { node: number, ws: number } | Latency metrics | | recentHistory | string[] | Recent track identifiers (for autoplay) |


const q = player.queue;
// Add tracks (returns new queue length)
q.add(track);
q.add(track, 0); // insert at position 0 (play next)
q.add([track1, track2]); // add multiple
// Remove
q.remove(index); // remove 1 track at index
q.remove(index, 3); // remove 3 tracks starting at index
// Reorder
q.move(fromIndex, toIndex);
q.shuffle();
// Clear
q.clear(); // clear tracks only
q.clear(true); // clear tracks + previous
// Skip to
q.skipTo(index); // shift to track at index, set as current
// Inspect
q.size; // number of queued tracks
q.first; // first track or null
q.last; // last track or null
q.totalDuration; // total duration including current (ms)
q.current; // currently playing track
q.previous; // previously played tracks (up to maxPreviousTracks)
// Functional
q.find(predicate);
q.filter(predicate);
q.map(callback);
q.some(predicate);
q.every(predicate);

const fm = player.filterManager;
// High-level presets
await fm.toggleNightcore();
await fm.toggleVaporwave();
await fm.setSpeed(1.2);
await fm.setPitch(1.1);
await fm.setVolume(0.8); // filter volume (0.0–5.0)
// EQ
await fm.setEQ([{ band: 0, gain: 0.5 }, { band: 1, gain: 0.3 }]);
await fm.setPreset('BassboostHigh');
await fm.setPreset('Nightcore');
await fm.setPreset('Vaporwave');
await fm.setPreset('Clear');
// Low-level
await fm.set('rotation', { rotationHz: 0.2 });
await fm.remove('rotation');
await fm.override({ timescale: { speed: 1.1, pitch: 1.2, rate: 1 } });
// Reset everything
await fm.resetFilters();
await fm.clear(); // alias for resetFilters
// Check state
fm.filters.nightcore; // boolean
fm.filters.vaporwave; // boolean
fm.filters.rotation; // boolean
fm.checkFiltersState(); // recompute filter flags

// Direct URL
const result = await manager.search({ query: 'https://open.spotify.com/track/...' }, user);
// Search with prefix
const result2 = await manager.search({ query: 'spsearch:daft punk' }, user);
// Add all results from a playlist
if (result.loadType === 'playlist') {
player.queue.add(result.tracks);
} else {
player.queue.add(result.tracks[0]);
}
if (!player.playing) await player.play();
// Get least-used nodes
const nodes = manager.nodeManager.leastUsedNodes('weighted');
// Create a node at runtime
manager.nodeManager.createNode({ id: 'extra', host: 'node2.example.com', port: 2333, authorization: 'pw' });
// Delete a node (moves players if movePlayers=true)
manager.nodeManager.deleteNode('extra', true);
// Disconnect/reconnect all
await manager.nodeManager.disconnectAll();
await manager.nodeManager.connectAll();
await manager.nodeManager.reconnectAll();
await player.setSponsorBlock(['sponsor', 'selfpromo', 'intro', 'outro']);
const segments = await player.getSponsorBlock();
await player.deleteSponsorBlock();
// Via node (lavalyrics-plugin)
const lyrics = await player.node.lyrics.get(track);
const current = await player.node.lyrics.getCurrent(player.guildId);
await player.node.lyrics.subscribe(player.guildId);
await player.node.lyrics.unsubscribe(player.guildId);
// Convenience wrappers on player
const lyrics2 = await player.getLyrics(track);
const current2 = await player.getCurrentLyrics();
player.subscribeLyrics();
player.unsubscribeLyrics();