DisGoLink provides a comprehensive event system for monitoring player state, track lifecycle, and connection status.
Event Listener Interface
Implement the EventListener interface to handle events:
type EventListener interface {
OnEvent(player Player, event lavalink.Message)
}
OnEvent
func(Player, lavalink.Message)
Called for every event from LavalinkParameters:
player: The player that triggered the event (nil for Stats events)
event: The event message (can be type-asserted to specific event types)
Registering Listeners
At Client Creation
client := disgolink.New(userID,
disgolink.WithListeners(myListener),
)
After Creation
client.AddListeners(listener1, listener2)
Typed Listener Function
Register a function that only handles specific event types:
client.AddListeners(disgolink.NewListenerFunc(func(p disgolink.Player, e lavalink.TrackStartEvent) {
fmt.Printf("Track started: %s\n", e.Track.Info.Title)
}))
// Or at creation
client := disgolink.New(userID,
disgolink.WithListenerFunc(func(p disgolink.Player, e lavalink.TrackEndEvent) {
fmt.Printf("Track ended: %s (reason: %s)\n", e.Track.Info.Title, e.Reason)
}),
)
Typed listener functions automatically filter events by type, so you donβt need manual type assertions.
Removing Listeners
client.RemoveListeners(listener1, listener2)
Event Types
TrackStartEvent
Fired when a track starts playing:
type TrackStartEvent struct {
Track Track `json:"track"`
GuildID_ snowflake.ID `json:"guildId"`
}
The track that started playing
The guild ID where playback started
Example Handler:
func (l *MyListener) OnEvent(p disgolink.Player, e lavalink.Message) {
if event, ok := e.(lavalink.TrackStartEvent); ok {
fmt.Printf("βΆοΈ Now playing: %s by %s\n",
event.Track.Info.Title,
event.Track.Info.Author)
}
}
TrackEndEvent
Fired when a track stops playing:
type TrackEndEvent struct {
Track Track `json:"track"`
Reason TrackEndReason `json:"reason"`
GuildID_ snowflake.ID `json:"guildId"`
}
The guild ID where playback ended
Track End Reasons
Track finished playing normally (use to play next track)
Track failed to load (use to play next track)
Track was stopped manually
Track was replaced by another track
Track stopped due to cleanup
Example Handler:
if event, ok := e.(lavalink.TrackEndEvent); ok {
fmt.Printf("βΉοΈ Track ended: %s (reason: %s)\n",
event.Track.Info.Title,
event.Reason)
// Play next track if this one finished
if event.Reason.MayStartNext() {
// Load and play next track
playNextTrack(p)
}
}
Use reason.MayStartNext() to determine if you should start the next track. Returns true for finished and loadFailed.
TrackExceptionEvent
Fired when a track encounters an error during playback:
type TrackExceptionEvent struct {
Track Track `json:"track"`
Exception Exception `json:"exception"`
GuildID_ snowflake.ID `json:"guildId"`
}
The track that encountered an error
Error details (message, severity, cause, stack trace)
The guild ID where the exception occurred
Example Handler:
if event, ok := e.(lavalink.TrackExceptionEvent); ok {
fmt.Printf("β Track error: %s - %s (severity: %s)\n",
event.Track.Info.Title,
event.Exception.Message,
event.Exception.Severity)
}
TrackStuckEvent
Fired when a track gets stuck (no audio packets for a period):
type TrackStuckEvent struct {
Track Track `json:"track"`
Threshold Duration `json:"thresholdMs"`
GuildID_ snowflake.ID `json:"guildId"`
}
How long the track was stuck in milliseconds
The guild ID where the track got stuck
Example Handler:
if event, ok := e.(lavalink.TrackStuckEvent); ok {
fmt.Printf("β οΈ Track stuck: %s (threshold: %dms)\n",
event.Track.Info.Title,
event.Threshold)
// Skip to next track
playNextTrack(p)
}
WebSocketClosedEvent
Fired when the Discord voice WebSocket connection closes:
type WebSocketClosedEvent struct {
Code int `json:"code"`
Reason string `json:"reason"`
ByRemote bool `json:"byRemote"`
GuildID_ snowflake.ID `json:"guildId"`
}
WebSocket close code (e.g., 4014 for kicked from voice channel)
Human-readable close reason
Whether the connection was closed by Discord (true) or locally (false)
The guild ID where the connection closed
Example Handler:
if event, ok := e.(lavalink.WebSocketClosedEvent); ok {
fmt.Printf("π Voice connection closed: %s (code: %d, remote: %v)\n",
event.Reason,
event.Code,
event.ByRemote)
}
Common close codes:
4014: Disconnected (kicked from channel)
4015: Voice server crashed
4006: Session no longer valid
PlayerUpdateMessage
Fired periodically with player state updates:
type PlayerUpdateMessage struct {
State PlayerState `json:"state"`
GuildID snowflake.ID `json:"guildId"`
}
Current player state (time, position, connected status, ping)
The guild ID for this player
Example Handler:
if event, ok := e.(lavalink.PlayerUpdateMessage); ok {
fmt.Printf("π Player update: position=%dms, ping=%dms, connected=%v\n",
event.State.Position,
event.State.Ping,
event.State.Connected)
}
Player updates are sent approximately every 5 seconds while a track is playing.
PlayerPauseEvent
Fired when a player is paused:
type PlayerPauseEvent struct {
GuildID_ snowflake.ID `json:"guildId"`
}
This event is not sent by Lavalink. Itβs dispatched artificially by DisGoLink when calling player.Update(ctx, lavalink.WithPaused(true)).
Example Handler:
if event, ok := e.(lavalink.PlayerPauseEvent); ok {
fmt.Printf("βΈοΈ Player paused in guild %d\n", event.GuildID_)
}
PlayerResumeEvent
Fired when a player is resumed:
type PlayerResumeEvent struct {
GuildID_ snowflake.ID `json:"guildId"`
}
This event is not sent by Lavalink. Itβs dispatched artificially by DisGoLink when calling player.Update(ctx, lavalink.WithPaused(false)).
Example Handler:
if event, ok := e.(lavalink.PlayerResumeEvent); ok {
fmt.Printf("βΆοΈ Player resumed in guild %d\n", event.GuildID_)
}
StatsMessage
Fired periodically with node statistics:
Contains CPU usage, memory usage, uptime, player count, and frame statistics.
Example Handler:
if event, ok := e.(lavalink.StatsMessage); ok {
stats := lavalink.Stats(event)
fmt.Printf("π Node stats: CPU=%.2f%%, Memory=%dMB, Players=%d\n",
stats.CPU.SystemLoad*100,
stats.Memory.Used/1024/1024,
stats.Players)
}
For StatsMessage events, the player parameter passed to OnEvent() is nil since stats are node-level, not player-specific.
Complete Example
Hereβs a complete event listener implementation:
type MyEventListener struct {
queue map[snowflake.ID][]lavalink.Track // guild ID -> track queue
mu sync.RWMutex
}
func (l *MyEventListener) OnEvent(p disgolink.Player, e lavalink.Message) {
switch event := e.(type) {
case lavalink.TrackStartEvent:
fmt.Printf("βΆοΈ [%d] Now playing: %s\n",
event.GuildID_,
event.Track.Info.Title)
case lavalink.TrackEndEvent:
fmt.Printf("βΉοΈ [%d] Track ended: %s\n",
event.GuildID_,
event.Reason)
if event.Reason.MayStartNext() {
l.playNext(p)
}
case lavalink.TrackExceptionEvent:
fmt.Printf("β [%d] Track error: %s\n",
event.GuildID_,
event.Exception.Message)
l.playNext(p)
case lavalink.TrackStuckEvent:
fmt.Printf("β οΈ [%d] Track stuck for %dms\n",
event.GuildID_,
event.Threshold)
l.playNext(p)
case lavalink.WebSocketClosedEvent:
fmt.Printf("π [%d] Voice disconnected: %s (code %d)\n",
event.GuildID_,
event.Reason,
event.Code)
case lavalink.PlayerPauseEvent:
fmt.Printf("βΈοΈ [%d] Paused\n", event.GuildID_)
case lavalink.PlayerResumeEvent:
fmt.Printf("βΆοΈ [%d] Resumed\n", event.GuildID_)
case lavalink.PlayerUpdateMessage:
// Update UI with current position
if p != nil && p.Track() != nil {
progress := float64(event.State.Position) / float64(p.Track().Info.Length) * 100
fmt.Printf("π [%d] Progress: %.1f%%\n", event.GuildID, progress)
}
case lavalink.StatsMessage:
stats := lavalink.Stats(event)
if stats.CPU.LavalinkLoad > 0.8 {
fmt.Printf("β οΈ High CPU usage: %.2f%%\n", stats.CPU.LavalinkLoad*100)
}
}
}
func (l *MyEventListener) playNext(p disgolink.Player) {
l.mu.Lock()
defer l.mu.Unlock()
queue := l.queue[p.GuildID()]
if len(queue) == 0 {
return
}
next := queue[0]
l.queue[p.GuildID()] = queue[1:]
_ = p.Update(context.Background(), lavalink.WithTrack(next))
}
// Register the listener
client := disgolink.New(userID,
disgolink.WithListeners(&MyEventListener{
queue: make(map[snowflake.ID][]lavalink.Track),
}),
)
Best Practices
Event handlers should be fast and non-blocking. Long operations should be done in goroutines.
// Bad: blocking event handler
func (l *MyListener) OnEvent(p disgolink.Player, e lavalink.Message) {
if event, ok := e.(lavalink.TrackStartEvent); ok {
time.Sleep(5 * time.Second) // Blocks all events!
sendDiscordMessage(event.Track.Info.Title)
}
}
// Good: non-blocking event handler
func (l *MyListener) OnEvent(p disgolink.Player, e lavalink.Message) {
if event, ok := e.(lavalink.TrackStartEvent); ok {
go sendDiscordMessage(event.Track.Info.Title) // Non-blocking
}
}
DisGoLink recovers from panics in event handlers and logs them, but itβs best practice to handle errors gracefully.