Skip to main content

Quickstart Guide

This guide will walk you through creating a functional Discord music bot using DisGoLink and DisGo. By the end, you’ll have a bot that can join voice channels and play music from YouTube.
This guide uses DisGo as the Discord library. If you’re using DiscordGo or another library, the voice event handling will be slightly different, but the DisGoLink API remains the same.

Complete Working Example

1

Import Required Packages

Start by importing DisGoLink and your Discord library:
import (
    "context"
    "log/slog"
    
    "github.com/disgoorg/disgo"
    "github.com/disgoorg/disgo/bot"
    "github.com/disgoorg/disgo/events"
    "github.com/disgoorg/disgo/gateway"
    "github.com/disgoorg/snowflake/v2"
    
    "github.com/disgoorg/disgolink/v3/disgolink"
    "github.com/disgoorg/disgolink/v3/lavalink"
)
2

Create the Lavalink Client

Create a DisGoLink client with your bot’s user ID:
var userID = snowflake.ID(1234567890)

lavalinkClient := disgolink.New(userID,
    disgolink.WithListenerFunc(onPlayerUpdate),
    disgolink.WithListenerFunc(onPlayerPause),
    disgolink.WithListenerFunc(onPlayerResume),
    disgolink.WithListenerFunc(onTrackStart),
    disgolink.WithListenerFunc(onTrackEnd),
    disgolink.WithListenerFunc(onTrackException),
    disgolink.WithListenerFunc(onTrackStuck),
    disgolink.WithListenerFunc(onWebSocketClosed),
)
The user ID should be your bot’s Discord application ID, which you can get from client.ApplicationID() after creating your Discord client.
3

Forward Voice Events

DisGoLink needs to receive voice state and voice server updates from Discord. Set up event handlers to forward these events:
client, err := disgo.New(token,
    bot.WithGatewayConfigOpts(
        gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildVoiceStates),
    ),
    bot.WithEventListenerFunc(onVoiceStateUpdate),
    bot.WithEventListenerFunc(onVoiceServerUpdate),
)

func onVoiceStateUpdate(event *events.GuildVoiceStateUpdate) {
    // Filter all non-bot voice state updates
    if event.VoiceState.UserID != client.ApplicationID() {
        return
    }
    lavalinkClient.OnVoiceStateUpdate(
        context.TODO(),
        event.VoiceState.GuildID,
        event.VoiceState.ChannelID,
        event.VoiceState.SessionID,
    )
}

func onVoiceServerUpdate(event *events.VoiceServerUpdate) {
    lavalinkClient.OnVoiceServerUpdate(
        context.TODO(),
        event.GuildID,
        event.Token,
        *event.Endpoint,
    )
}
Make sure to filter voice state updates to only your bot’s updates, otherwise you’ll forward events for all users!
4

Add a Lavalink Node

Connect to your Lavalink server by adding a node:
node, err := lavalinkClient.AddNode(context.TODO(), disgolink.NodeConfig{
    Name:      "test",           // A unique node name
    Address:   "localhost:2333", // Lavalink server address
    Password:  "youshallnotpass", // Lavalink server password
    Secure:    false,            // Use 'false' for ws://, 'true' for wss://
    SessionID: "",               // Optional: resume a previous session
})
if err != nil {
    log.Fatal("Failed to add node:", err)
}
The AddNode method is a blocking call that connects to the Lavalink server. Make sure your Lavalink server is running before calling this.
5

Load a Track

Before playing audio, you need to resolve tracks using Lavalink’s search or URL loading:
query := "ytsearch:Rick Astley - Never Gonna Give You Up"

var toPlay *lavalink.Track
lavalinkClient.BestNode().LoadTracksHandler(context.TODO(), query, disgolink.NewResultHandler(
    func(track lavalink.Track) {
        // Loaded a single track (from URL)
        toPlay = &track
        log.Println("Loaded track:", track.Info.Title)
    },
    func(playlist lavalink.Playlist) {
        // Loaded a playlist
        log.Println("Loaded playlist:", playlist.Info.Name)
        if len(playlist.Tracks) > 0 {
            toPlay = &playlist.Tracks[0]
        }
    },
    func(tracks []lavalink.Track) {
        // Loaded search results
        log.Println("Found", len(tracks), "tracks")
        if len(tracks) > 0 {
            toPlay = &tracks[0]
        }
    },
    func() {
        // No matches found
        log.Println("No matches found for query:", query)
    },
    func(err error) {
        // Error loading tracks
        log.Println("Error loading tracks:", err)
    },
))
Supported query formats:
  • ytsearch:query - Search YouTube
  • ytmsearch:query - Search YouTube Music
  • scsearch:query - Search SoundCloud
  • Direct URLs from supported platforms
6

Join Voice Channel and Play

Connect to a voice channel and start playback:
// Join the voice channel (DisGo)
err := client.UpdateVoiceState(context.TODO(), guildID, channelID, false, false)
if err != nil {
    log.Fatal("Failed to join voice channel:", err)
}

// Get or create a player for this guild
player := lavalinkClient.Player(guildID)

// Play the track
err = player.Update(context.TODO(), lavalink.WithTrack(*toPlay))
if err != nil {
    log.Fatal("Failed to play track:", err)
}
For other Discord libraries:
err := client.UpdateVoiceState(context.TODO(), guildID, channelID, false, false)
7

Implement Event Handlers

React to player events to implement features like auto-play, queue management, and error handling:
func onTrackStart(player disgolink.Player, event lavalink.TrackStartEvent) {
    log.Printf("Track started: %s\n", event.Track.Info.Title)
}

func onTrackEnd(player disgolink.Player, event lavalink.TrackEndEvent) {
    log.Printf("Track ended: %s (reason: %s)\n", event.Track.Info.Title, event.Reason)
    
    // If track finished normally, you might want to play the next track
    if event.Reason == lavalink.TrackEndReasonFinished {
        // Play next track from queue
    }
}

func onTrackException(player disgolink.Player, event lavalink.TrackExceptionEvent) {
    log.Printf("Track exception: %s - %s\n", event.Exception.Message, event.Exception.Cause)
}

func onTrackStuck(player disgolink.Player, event lavalink.TrackStuckEvent) {
    log.Printf("Track stuck: threshold %dms\n", event.ThresholdMs)
}

func onPlayerUpdate(player disgolink.Player, event lavalink.PlayerUpdateMessage) {
    // Periodic updates with player state (default: every 5 seconds)
    log.Printf("Position: %s\n", event.State.Position)
}

func onPlayerPause(player disgolink.Player, event lavalink.PlayerPauseEvent) {
    log.Println("Player paused")
}

func onPlayerResume(player disgolink.Player, event lavalink.PlayerResumeEvent) {
    log.Println("Player resumed")
}

func onWebSocketClosed(player disgolink.Player, event lavalink.WebSocketClosedEvent) {
    log.Printf("WebSocket closed: code=%d, reason=%s, byRemote=%v\n",
        event.Code, event.Reason, event.ByRemote)
}

Full Example Code

Here’s a complete minimal bot based on the DisGo example from the source:
main.go
package main

import (
    "context"
    "log"
    "log/slog"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/disgoorg/disgo"
    "github.com/disgoorg/disgo/bot"
    "github.com/disgoorg/disgo/events"
    "github.com/disgoorg/disgo/gateway"
    "github.com/disgoorg/disgo/cache"
    "github.com/disgoorg/snowflake/v2"

    "github.com/disgoorg/disgolink/v3/disgolink"
    "github.com/disgoorg/disgolink/v3/lavalink"
)

var (
    token        = os.Getenv("DISCORD_TOKEN")
    lavalinkAddr = os.Getenv("LAVALINK_ADDRESS") // e.g., "localhost:2333"
    lavalinkPass = os.Getenv("LAVALINK_PASSWORD")
)

var lavalinkClient disgolink.Client

func main() {
    slog.Info("Starting DisGoLink bot...")

    // Create Discord client
    client, err := disgo.New(token,
        bot.WithGatewayConfigOpts(
            gateway.WithIntents(gateway.IntentGuilds, gateway.IntentGuildVoiceStates),
        ),
        bot.WithCacheConfigOpts(
            cache.WithCaches(cache.FlagVoiceStates),
        ),
        bot.WithEventListenerFunc(onVoiceStateUpdate),
        bot.WithEventListenerFunc(onVoiceServerUpdate),
    )
    if err != nil {
        log.Fatal("Failed to create Discord client:", err)
    }

    // Create Lavalink client
    lavalinkClient = disgolink.New(client.ApplicationID(),
        disgolink.WithListenerFunc(onTrackStart),
        disgolink.WithListenerFunc(onTrackEnd),
        disgolink.WithListenerFunc(onTrackException),
    )

    // Connect to Discord
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err = client.OpenGateway(ctx); err != nil {
        log.Fatal("Failed to connect to Discord:", err)
    }
    defer client.Close(context.TODO())

    // Add Lavalink node
    _, err = lavalinkClient.AddNode(ctx, disgolink.NodeConfig{
        Name:     "main",
        Address:  lavalinkAddr,
        Password: lavalinkPass,
        Secure:   false,
    })
    if err != nil {
        log.Fatal("Failed to add Lavalink node:", err)
    }

    slog.Info("Bot is running. Press Ctrl+C to exit.")

    // Wait for interrupt signal
    s := make(chan os.Signal, 1)
    signal.Notify(s, syscall.SIGINT, syscall.SIGTERM)
    <-s
}

func onVoiceStateUpdate(event *events.GuildVoiceStateUpdate) {
    if event.VoiceState.UserID != event.Bot().ApplicationID() {
        return
    }
    lavalinkClient.OnVoiceStateUpdate(
        context.TODO(),
        event.VoiceState.GuildID,
        event.VoiceState.ChannelID,
        event.VoiceState.SessionID,
    )
}

func onVoiceServerUpdate(event *events.VoiceServerUpdate) {
    lavalinkClient.OnVoiceServerUpdate(
        context.TODO(),
        event.GuildID,
        event.Token,
        *event.Endpoint,
    )
}

func onTrackStart(player disgolink.Player, event lavalink.TrackStartEvent) {
    slog.Info("Track started", "title", event.Track.Info.Title)
}

func onTrackEnd(player disgolink.Player, event lavalink.TrackEndEvent) {
    slog.Info("Track ended", "title", event.Track.Info.Title, "reason", event.Reason)
}

func onTrackException(player disgolink.Player, event lavalink.TrackExceptionEvent) {
    slog.Error("Track exception", "error", event.Exception.Message)
}

Player Control

Once you have a player, you can control it with various options:
// Pause playback
err := player.Update(context.TODO(), lavalink.WithPaused(true))

// Resume playback
err = player.Update(context.TODO(), lavalink.WithPaused(false))

Troubleshooting

Bot Joins But No Audio Plays

  • Verify your Lavalink server is running and accessible
  • Check that you’re forwarding voice events correctly
  • Ensure the track loaded successfully before calling Update
  • Check Lavalink server logs for errors

”No Available Nodes” Error

This means BestNode() returned nil. Ensure:
  • You’ve called AddNode successfully
  • The node connection didn’t fail
  • You’re calling AddNode before trying to load tracks

Type Conversion Errors with Other Libraries

If using DiscordGo or other libraries:
// Convert string to Snowflake
guildID := snowflake.MustParse(guildIDString)

// Convert Snowflake to string
guildIDString := guildID.String()

Track Won’t Load

  • Ensure your query format is correct (e.g., ytsearch:query)
  • Check if the source is enabled in your Lavalink configuration
  • Verify the URL is from a supported platform
  • Check Lavalink server logs for source-specific errors

Next Steps

You now have a working Discord music bot! Here are some ideas to expand it:
  • Implement a queue system for multiple tracks
  • Add slash commands for play, pause, skip, and volume control
  • Create playlist support
  • Add audio filters and effects
  • Implement session resuming for bot restarts
  • Integrate plugins for additional features

Full Examples

Explore complete example bots with DisGo and DiscordGo

API Reference

Deep dive into the full DisGoLink API

Get Help

Join the Discord server for support

Plugin System

Learn how to extend DisGoLink with plugins

Build docs developers (and LLMs) love