Feature-rich Lavalink v4 client for Luvit (Lua) Discord bots - multi-node support, queue management, audio filters, session resuming and Discordia / discord.lua integrations.
lit install filispeen/lavalink.luaFor the example bot, also install Discordia:
lit install SinisterRectus/discordialavalink.lua currently supports ONLY my custom Discord API wrapper filispeen/discord.lua and SinisterRectus/discordia (first supported library).
- Luvit runtime + Lit package manager
- Lavalink v4 server (or Lavalink v3.7+ with
LAVALINK_API_VERSION=3)
local discordia = require("discordia")
local lavalinklua = require("lavalink.lua")
local client = discordia.Client()
client:on("ready", function()
local lavalink = lavalinklua.discordia(client, {
clientId = client.user.id,
nodes = {
{
id = "main",
host = "localhost",
port = 2333,
authorization = "youshallnotpass",
},
},
})
lavalink:on("trackStart", function(player, track)
print("Now playing: " .. track.info.title)
end)
lavalink:on("queueEnd", function(player)
player:destroy("queue finished")
end)
lavalink:init()
end)
client:run("Bot TOKEN")Or with discord.lua instead of Discordia:
local Bot = require("discord.lua")
local lavalinklua = require("lavalink.lua")
local bot = Bot("Bot TOKEN")
bot:on("ready", function()
local lavalink = lavalinklua.discord(bot, {
nodes = {
{
id = "main",
host = "localhost",
port = 2333,
authorization = "youshallnotpass",
},
},
})
lavalink:on("trackStart", function(player, track)
print("Now playing: " .. track.info.title)
end)
lavalink:on("queueEnd", function(player)
player:destroy("queue finished")
end)
lavalink:init()
end)
bot:run()Playing a track:
local player, created = lavalink:createPlayer({
guildId = guildId,
voiceChannelId = voiceChannelId,
textChannelId = textChannelId,
selfDeaf = true,
})
if created then player:connect() end
local result = lavalink:search("never gonna give you up")
player.queue:add(result.data[1])
player:play()| Event | Arguments | Description |
|---|---|---|
nodeReady |
node, resumed, sessionId |
Node WebSocket ready |
nodeConnect |
node |
WebSocket connection opened |
nodeDisconnect |
node, reason |
WebSocket disconnected |
nodeReconnecting |
node, attempt, delayMs |
Reconnect scheduled |
nodeError |
node, error |
Node-level error |
nodeStats |
node, stats |
Periodic server stats |
playerCreate |
player |
Player created |
playerDestroy |
player, reason |
Player destroyed |
playerUpdate |
player, state |
Position/ping update |
playerPause |
player |
Player paused |
playerResume |
player |
Player resumed |
playerRepeat |
player, mode |
Repeat mode changed |
playerMoved |
player, oldNode, newNode |
Player moved to new node |
trackStart |
player, track |
Track began playing |
trackEnd |
player, track, reason |
Track ended |
trackError |
player, track, error |
Track exception / load failed |
trackStuck |
player, track, thresholdMs |
Track stuck |
queueEnd |
player |
Queue finished |
socketClosed |
player, code, reason, byRemote |
Discord voice WS closed |
error |
player, error |
Generic player error |
nodeLinkReady |
node, info |
Node was identified as NodeLink |
sponsorBlockSegmentsLoaded |
player, segments, data |
NodeLink loaded SponsorBlock segments |
sponsorBlockSegmentSkipped |
player, segment, data |
NodeLink skipped a segment |
mixStart / mixEnd |
player, data |
NodeLink mixer lifecycle event |
lyricsFound |
player, lyrics, data |
Lyrics were resolved for the current track |
lyricsLine |
player, lineIndex, data |
Synchronized lyrics advanced a line |
voiceReceiveFrame |
node, guildId, frame, message |
Raw NodeLink Opus or PCM voice frame |
nodeLinkEvent |
node, player?, data |
A NodeLink event without a dedicated convenience event |
player:connect() -- Send OP4 to Discord (join voice)
player:disconnect(destroyPlayer?) -- Leave voice channel
player:destroy(reason?) -- Delete player on Lavalink + cleanup
player:play(options?) -- Play current/next track
player:pause(state?) -- Toggle or set pause
player:resume() -- Alias for pause(false)
player:stop() -- Stop without clearing queue
player:stopPlaying(clearQueue?) -- Stop + optionally clear queue
player:skip(skipTo?, throwError?) -- Skip to Nth track
player:seek(positionMs) -- Seek to position
player:setVolume(0-1000) -- Set volume
player:setRepeatMode("off"|"track"|"queue")
player:moveToNode(nodeId) -- Live-migrate to another node
player:getPosition() -- Client-side interpolated position (ms)
-- Queue
player.queue:add(track|tracks)
player.queue:remove(startIndex, endIndex?)
player.queue:shuffle()
player.queue:clear()
player.queue.current -- current track
player.queue.tracks -- upcoming tracks
player.queue.previous -- last 25 played
-- Filters
player.filters:setVolume(multiplier)
player.filters:setEqualizer(bands) -- { {band=0, gain=0.35}, ... }
player.filters:setTimescale({ speed, pitch, rate })
player.filters:setRotation({ rotationHz })
player.filters:setKaraoke(options)
player.filters:setTremolo(options)
player.filters:setVibrato(options)
player.filters:setDistortion(options)
player.filters:setChannelMix(options)
player.filters:setLowPass(options)
player.filters:setPluginFilters(table)
player.filters:resetFilters()
player.filters:resetFilter(filterName)
player.filters:apply() -- Re-send current filter state to Lavalink
-- NodeLink filters
player.filters:setEcho({ delay = 500, feedback = 0.3, mix = 0.5 })
player.filters:setChorus({ rate = 1.5, depth = 0.4, delay = 20, mix = 0.5 })
player.filters:setCompressor({ threshold = -18, ratio = 3, attack = 10, release = 100, gain = 0 })
player.filters:setHighPass({ smoothing = 2.0 })
player.filters:setPhaser({ stages = 4, rate = 0.5, depth = 0.6, feedback = 0.3, mix = 0.5 })
player.filters:setSpatial(options)lavalink:addNode(options) -- Add a node at runtime
lavalink:removeNode(id) -- Disconnect and remove a node
lavalink:init() -- Connect all configured nodes
lavalink:getNode(id?) -- Get node by id, or least-loaded usable node
lavalink:getUsableNodes() -- List of connected + ready nodes
lavalink:getAllNodes() -- List of all nodes regardless of state
lavalink:createPlayer(options) -- Create (or get existing) player for a guild
lavalink:getPlayer(guildId) -- Get existing player, or nil
lavalink:destroyPlayer(guildId, reason?) -- Destroy player for a guild
lavalink:search(query, options?) -- REST loadTracks, options = { source?, node? }
lavalink:decodeTrack(encoded, nodeId?)
lavalink:decodeTracks(encodedList, nodeId?)
-- NodeLink helpers
lavalink:refreshNodeInfo(nodeId?)
lavalink:getNodeLinkConnection(nodeId?)
lavalink:getNodeLinkWorkers(nodeId?)
lavalink:patchNodeLinkWorker(data, nodeId?)
lavalink:getLyrics(encodedTrack, { node?, lang? })
lavalink:getChapters(encodedTrack, nodeId?)
lavalink:getMeaning(encodedTrack, { node?, lang? })
lavalink:getTrackStream(encodedTrack, { node?, itag? })
lavalink:handleVoiceUpdate(packet) -- Feed raw VOICE_STATE_UPDATE / VOICE_SERVER_UPDATEWhen a node becomes ready, node.isNodeLink is set from /v4/info; listen to
nodeLinkReady if the application needs to wait for that detection. All NodeLink
methods are opt-in, so normal Lavalink v4 nodes are unaffected.
-- Metadata for the current track
local lyrics = player:getLyrics("uk")
local chapters = player:getChapters()
-- SponsorBlock
player:updateSponsorBlock({ enabled = true, categories = { "sponsor", "intro" } })
player:setSponsorBlockSegments(segments) -- optional custom segments
player:subscribeLyrics(true) -- emits lyricsFound / lyricsLine
-- Overlay a TTS/SFX/music track. `track` can be a result from loadtracks.
local mix = player:addMix(track, 0.8)
player:updateMix(mix.mixId, 0.5)
player:removeMix(mix.mixId)
-- Select an alternate audio track and preload the next encoded track.
player:play({ audioTrackId = "en.4", nextTrack = nextTrack })
-- Experimental voice receive: configure voiceReceive in NodeLink first.
player:startVoiceReceive(function(guildId, frame)
-- frame is a binary Opus or PCM S16LE payload, per NodeLink configuration.
end)The additional source prefixes (for example spsearch:, dzsearch:, gtts:
and search:) already work through lavalink:search, because its source
option is passed directly to NodeLink:
local result = lavalink:search("never gonna give you up", { source = "spsearch" })createPlayer options:
{
guildId = "...", -- required
voiceChannelId = "...",
textChannelId = "...",
selfDeaf = true, -- default true
selfMute = false,
node = "main", -- optional node id, defaults to least-loaded
region = "europe",
volume = 100,
}{
id = "main", -- defaults to "host:port"
host = "localhost",
port = 2333,
authorization = "youshallnotpass",
secure = false, -- use wss/https
resuming = true, -- enable session resuming
resumeTimeout = 60, -- seconds
reconnectTries = 5,
reconnectDelay = 5000, -- ms, doubles on each attempt up to 60s
regions = { "eu-west" }, -- used by region-aware node selection
}The client uses Lavalink v4 by default. To use a Lavalink v3.7+ node, set the environment variable before starting the bot:
LAVALINK_API_VERSION=3 luvit bot.luaOr choose a version in code; a node-level apiVersion overrides the manager
default:
local lavalink = lavalinklua.discordia(client, {
apiVersion = 3, -- accepts only 3 or 4; default is 4
-- nodes = { { apiVersion = 4, ... } }, -- optional per-node override
})If you're not using the Discordia or discord.lua integration, call lavalink:handleVoiceUpdate(packet) yourself for every VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE gateway event, and provide sendPayload = function(guildId, payload) ... end in the manager options to forward voice payloads (OP4) to your gateway.
The example/ folder contains two fully working bots with the same music commands, one per integration.
Discordia:
cd example/discordia
cp .env.example .env
# fill in .env with your values
luvit bot.luadiscord.lua:
cd example/discord.lua
cp .env.example .env
# fill in .env with your values
luvit bot.luaMIT