0
# Events & Exceptions
1
2
Rich event system for track lifecycle, player state changes, and WebSocket events, plus comprehensive exception hierarchy for robust error handling. The event system enables responsive music bots that can react to playback changes, while the exception hierarchy provides detailed error information for debugging and user feedback.
3
4
## Capabilities
5
6
### Exception Hierarchy
7
8
Comprehensive exception system for handling various error conditions in wavelink operations.
9
10
```python { .api }
11
# Base exception class
12
class WavelinkException(Exception):
13
"""
14
Base wavelink exception class.
15
16
All wavelink exceptions derive from this exception, making it easy
17
to catch any wavelink-related error.
18
"""
19
20
# Node-related exceptions
21
class NodeException(WavelinkException):
22
"""
23
Generic Node error with optional HTTP status code.
24
25
Attributes:
26
- status: int | None - HTTP status code if available
27
"""
28
def __init__(self, msg: str | None = None, status: int | None = None):
29
"""
30
Initialize NodeException.
31
32
Parameters:
33
- msg: Error message
34
- status: HTTP status code
35
"""
36
37
status: int | None
38
39
class InvalidClientException(WavelinkException):
40
"""
41
Exception raised when an invalid discord.Client is provided
42
while connecting a wavelink.Node.
43
"""
44
45
class AuthorizationFailedException(WavelinkException):
46
"""
47
Exception raised when Lavalink fails to authenticate a Node
48
with the provided password.
49
"""
50
51
class InvalidNodeException(WavelinkException):
52
"""
53
Exception raised when a Node is tried to be retrieved from the
54
Pool without existing, or the Pool is empty.
55
"""
56
57
# Lavalink server exceptions
58
class LavalinkException(WavelinkException):
59
"""
60
Exception raised when Lavalink returns an invalid response.
61
62
Attributes:
63
- timestamp: int - Error timestamp
64
- status: int - HTTP response status code
65
- error: str - Error message from Lavalink
66
- trace: str | None - Stack trace if available
67
- path: str - Request path that caused the error
68
"""
69
def __init__(self, msg: str | None = None, /, *, data: dict):
70
"""
71
Initialize LavalinkException from error response data.
72
73
Parameters:
74
- msg: Custom error message
75
- data: Error response data from Lavalink
76
"""
77
78
timestamp: int
79
status: int
80
error: str
81
trace: str | None
82
path: str
83
84
class LavalinkLoadException(WavelinkException):
85
"""
86
Exception raised when an error occurred loading tracks via Lavalink.
87
88
Attributes:
89
- error: str - Error message from Lavalink
90
- severity: str - Error severity level
91
- cause: str - Cause of the error
92
"""
93
def __init__(self, msg: str | None = None, /, *, data: dict):
94
"""
95
Initialize LavalinkLoadException from load error data.
96
97
Parameters:
98
- msg: Custom error message
99
- data: Load error data from Lavalink
100
"""
101
102
error: str
103
severity: str
104
cause: str
105
106
# Player-related exceptions
107
class InvalidChannelStateException(WavelinkException):
108
"""
109
Exception raised when a Player tries to connect to an invalid channel
110
or has invalid permissions to use this channel.
111
"""
112
113
class ChannelTimeoutException(WavelinkException):
114
"""
115
Exception raised when connecting to a voice channel times out.
116
"""
117
118
# Queue-related exceptions
119
class QueueEmpty(WavelinkException):
120
"""
121
Exception raised when you try to retrieve from an empty queue.
122
"""
123
124
# Cache-related exceptions
125
class CapacityZero(WavelinkException):
126
"""
127
Exception raised when LFU cache has zero capacity.
128
"""
129
```
130
131
### Event Payload Classes
132
133
Data structures containing information passed to event handlers for various wavelink events.
134
135
```python { .api }
136
# Node events
137
class NodeReadyEventPayload:
138
"""Data structure for node ready events."""
139
resumed: bool
140
session_id: str
141
142
# Track events
143
class TrackStartEventPayload:
144
"""Data structure for track start events."""
145
track: dict
146
player: Player
147
148
class TrackEndEventPayload:
149
"""Data structure for track end events."""
150
track: dict
151
reason: str
152
player: Player
153
154
class TrackExceptionEventPayload:
155
"""Data structure for track exception events."""
156
track: dict
157
exception: dict
158
player: Player
159
160
class TrackStuckEventPayload:
161
"""Data structure for track stuck events."""
162
track: dict
163
threshold_ms: int
164
player: Player
165
166
# WebSocket events
167
class WebsocketClosedEventPayload:
168
"""Data structure for WebSocket closed events."""
169
code: int
170
reason: str
171
by_remote: bool
172
player: Player
173
174
# Player events
175
class PlayerUpdateEventPayload:
176
"""Data structure for player update events."""
177
state: dict
178
player: Player
179
180
# Statistics events
181
class StatsEventMemory:
182
"""Memory statistics data."""
183
free: int
184
used: int
185
allocated: int
186
reservable: int
187
188
class StatsEventCPU:
189
"""CPU statistics data."""
190
cores: int
191
system_load: float
192
lavalink_load: float
193
194
class StatsEventFrames:
195
"""Frame statistics data."""
196
sent: int
197
nulled: int
198
deficit: int
199
200
class StatsEventPayload:
201
"""Node statistics event data."""
202
players: int
203
playing_players: int
204
uptime: int
205
memory: StatsEventMemory
206
cpu: StatsEventCPU
207
frame_stats: StatsEventFrames | None
208
209
# Response payloads
210
class StatsResponsePayload:
211
"""Statistics response from node."""
212
players: int
213
playing_players: int
214
uptime: int
215
memory: dict
216
cpu: dict
217
frame_stats: dict | None
218
219
class PlayerStatePayload:
220
"""Player state information."""
221
time: int
222
position: int
223
connected: bool
224
ping: int
225
226
class VoiceStatePayload:
227
"""Voice state information."""
228
token: str
229
endpoint: str
230
session_id: str
231
232
class PlayerResponsePayload:
233
"""Player response information."""
234
guild_id: str
235
track: dict | None
236
volume: int
237
paused: bool
238
state: PlayerStatePayload
239
voice: VoiceStatePayload
240
filters: dict
241
242
class GitResponsePayload:
243
"""Git information response."""
244
branch: str
245
commit: str
246
commit_time: int
247
248
class VersionResponsePayload:
249
"""Version information response."""
250
semver: str
251
major: int
252
minor: int
253
patch: int
254
pre_release: str | None
255
build: str | None
256
257
class PluginResponsePayload:
258
"""Plugin information response."""
259
name: str
260
version: str
261
262
class InfoResponsePayload:
263
"""Node information response."""
264
version: VersionResponsePayload
265
build_time: int
266
git: GitResponsePayload
267
jvm: str
268
lavaplayer: str
269
source_managers: list[str]
270
filters: list[str]
271
plugins: list[PluginResponsePayload]
272
273
class ExtraEventPayload:
274
"""Extra event data for custom events."""
275
data: dict
276
```
277
278
### Event Handler Methods
279
280
Event handlers that can be implemented in your bot to respond to wavelink events.
281
282
```python { .api }
283
# Event handler signatures for discord.py bots
284
async def on_wavelink_node_ready(payload: NodeReadyEventPayload) -> None:
285
"""Called when a node connects and is ready."""
286
287
async def on_wavelink_track_start(payload: TrackStartEventPayload) -> None:
288
"""Called when a track starts playing."""
289
290
async def on_wavelink_track_end(payload: TrackEndEventPayload) -> None:
291
"""Called when a track finishes playing."""
292
293
async def on_wavelink_track_exception(payload: TrackExceptionEventPayload) -> None:
294
"""Called when a track encounters an exception."""
295
296
async def on_wavelink_track_stuck(payload: TrackStuckEventPayload) -> None:
297
"""Called when a track gets stuck."""
298
299
async def on_wavelink_websocket_closed(payload: WebsocketClosedEventPayload) -> None:
300
"""Called when the WebSocket connection closes."""
301
302
async def on_wavelink_player_update(payload: PlayerUpdateEventPayload) -> None:
303
"""Called when player state updates."""
304
305
async def on_wavelink_stats_update(payload: StatsEventPayload) -> None:
306
"""Called when node statistics update."""
307
308
async def on_wavelink_extra_event(payload: ExtraEventPayload) -> None:
309
"""Called for custom events from Lavalink plugins."""
310
```
311
312
## Usage Examples
313
314
### Exception Handling
315
316
```python
317
import wavelink
318
import discord
319
from discord.ext import commands
320
321
@bot.event
322
async def on_command_error(ctx, error):
323
"""Global error handler for wavelink exceptions."""
324
if isinstance(error, commands.CommandInvokeError):
325
error = error.original
326
327
if isinstance(error, wavelink.WavelinkException):
328
# Handle wavelink-specific errors
329
if isinstance(error, wavelink.LavalinkLoadException):
330
embed = discord.Embed(
331
title="❌ Failed to Load Track",
332
description=f"**Error**: {error.error}\n**Cause**: {error.cause}",
333
color=discord.Color.red()
334
)
335
await ctx.send(embed=embed)
336
337
elif isinstance(error, wavelink.QueueEmpty):
338
await ctx.send("❌ The queue is empty!")
339
340
elif isinstance(error, wavelink.InvalidChannelStateException):
341
await ctx.send("❌ Cannot connect to that voice channel!")
342
343
elif isinstance(error, wavelink.ChannelTimeoutException):
344
await ctx.send("❌ Connection to voice channel timed out!")
345
346
elif isinstance(error, wavelink.AuthorizationFailedException):
347
await ctx.send("❌ Failed to authenticate with Lavalink server!")
348
349
elif isinstance(error, wavelink.NodeException):
350
embed = discord.Embed(
351
title="❌ Node Error",
352
description=f"Status Code: {error.status}" if error.status else "Unknown node error",
353
color=discord.Color.red()
354
)
355
await ctx.send(embed=embed)
356
357
else:
358
# Generic wavelink error
359
await ctx.send(f"❌ Wavelink error: {error}")
360
else:
361
# Handle non-wavelink errors
362
await ctx.send(f"❌ An error occurred: {error}")
363
364
@bot.command()
365
async def safe_play(ctx, *, query: str):
366
"""Play command with comprehensive error handling."""
367
try:
368
# Ensure player exists
369
if not ctx.voice_client:
370
if not ctx.author.voice:
371
return await ctx.send("❌ You're not in a voice channel!")
372
373
try:
374
player = await ctx.author.voice.channel.connect(cls=wavelink.Player)
375
except wavelink.InvalidChannelStateException:
376
return await ctx.send("❌ I don't have permission to connect to that channel!")
377
except wavelink.ChannelTimeoutException:
378
return await ctx.send("❌ Connection timed out!")
379
else:
380
player = ctx.voice_client
381
382
# Search for tracks
383
try:
384
tracks = await wavelink.Pool.fetch_tracks(query)
385
except wavelink.LavalinkLoadException as e:
386
return await ctx.send(f"❌ Search failed: {e.error}")
387
except wavelink.InvalidNodeException:
388
return await ctx.send("❌ No Lavalink nodes available!")
389
390
if not tracks:
391
return await ctx.send("❌ No tracks found!")
392
393
# Handle results
394
if isinstance(tracks, wavelink.Playlist):
395
added = player.queue.put(tracks)
396
await ctx.send(f"✅ Added playlist: **{tracks.name}** ({added} tracks)")
397
else:
398
track = tracks[0]
399
if player.playing:
400
player.queue.put(track)
401
await ctx.send(f"✅ Added to queue: **{track.title}**")
402
else:
403
await player.play(track)
404
await ctx.send(f"▶️ Now playing: **{track.title}**")
405
406
except wavelink.WavelinkException as e:
407
await ctx.send(f"❌ Wavelink error: {e}")
408
except Exception as e:
409
await ctx.send(f"❌ Unexpected error: {e}")
410
```
411
412
### Event Handling
413
414
```python
415
@bot.event
416
async def on_wavelink_node_ready(payload: wavelink.NodeReadyEventPayload):
417
"""Handle node ready events."""
418
print(f"Node is ready! Session ID: {payload.session_id}")
419
print(f"Resumed previous session: {payload.resumed}")
420
421
@bot.event
422
async def on_wavelink_track_start(payload: wavelink.TrackStartEventPayload):
423
"""Handle track start events."""
424
player = payload.player
425
track = player.current
426
427
if track:
428
# Send now playing message to the last channel
429
guild = player.guild
430
if guild:
431
# Find a suitable channel to send the message
432
channel = discord.utils.get(guild.text_channels, name='music')
433
if not channel:
434
channel = guild.text_channels[0] # Fallback to first channel
435
436
embed = discord.Embed(
437
title="🎵 Now Playing",
438
description=f"**{track.title}**\nby {track.author}",
439
color=discord.Color.green()
440
)
441
442
if track.artwork:
443
embed.set_thumbnail(url=track.artwork)
444
445
duration = f"{track.length // 60000}:{(track.length // 1000) % 60:02d}"
446
embed.add_field(name="Duration", value=duration, inline=True)
447
embed.add_field(name="Source", value=track.source.name, inline=True)
448
embed.add_field(name="Queue", value=f"{player.queue.count} tracks", inline=True)
449
450
try:
451
await channel.send(embed=embed)
452
except discord.HTTPException:
453
pass # Ignore if we can't send messages
454
455
@bot.event
456
async def on_wavelink_track_end(payload: wavelink.TrackEndEventPayload):
457
"""Handle track end events."""
458
player = payload.player
459
460
# Auto-play next track if queue isn't empty
461
if not player.queue.is_empty:
462
try:
463
next_track = player.queue.get()
464
await player.play(next_track)
465
except wavelink.QueueEmpty:
466
pass # Queue became empty between check and get
467
elif player.autoplay != wavelink.AutoPlayMode.disabled:
468
# Let AutoPlay handle track recommendation
469
pass
470
else:
471
# Disconnect after inactivity timeout
472
await asyncio.sleep(300) # Wait 5 minutes
473
if not player.playing and player.queue.is_empty:
474
await player.disconnect()
475
476
@bot.event
477
async def on_wavelink_track_exception(payload: wavelink.TrackExceptionEventPayload):
478
"""Handle track exceptions."""
479
player = payload.player
480
exception = payload.exception
481
482
print(f"Track exception in guild {player.guild.id if player.guild else 'Unknown'}")
483
print(f"Exception: {exception}")
484
485
# Try to play next track if available
486
if not player.queue.is_empty:
487
try:
488
next_track = player.queue.get()
489
await player.play(next_track)
490
except wavelink.QueueEmpty:
491
pass
492
493
@bot.event
494
async def on_wavelink_track_stuck(payload: wavelink.TrackStuckEventPayload):
495
"""Handle stuck tracks."""
496
player = payload.player
497
threshold = payload.threshold_ms
498
499
print(f"Track stuck for {threshold}ms in guild {player.guild.id if player.guild else 'Unknown'}")
500
501
# Skip to next track
502
try:
503
await player.skip()
504
except Exception:
505
pass
506
507
@bot.event
508
async def on_wavelink_websocket_closed(payload: wavelink.WebsocketClosedEventPayload):
509
"""Handle WebSocket disconnections."""
510
player = payload.player
511
code = payload.code
512
reason = payload.reason
513
514
print(f"WebSocket closed for guild {player.guild.id if player.guild else 'Unknown'}")
515
print(f"Code: {code}, Reason: {reason}, By Remote: {payload.by_remote}")
516
517
# Handle specific close codes
518
if code == 4014: # Disconnected
519
# Voice channel was deleted or bot was disconnected
520
try:
521
await player.disconnect()
522
except Exception:
523
pass
524
525
@bot.event
526
async def on_wavelink_player_update(payload: wavelink.PlayerUpdateEventPayload):
527
"""Handle player state updates."""
528
player = payload.player
529
state = payload.state
530
531
# Update any player tracking/UI if needed
532
# This event fires frequently, so be careful with heavy operations
533
pass
534
535
@bot.event
536
async def on_wavelink_stats_update(payload: wavelink.StatsEventPayload):
537
"""Handle node statistics updates."""
538
stats = payload
539
540
print(f"Node Stats - Players: {stats.players}, Playing: {stats.playing_players}")
541
print(f"Memory: {stats.memory.used}MB used, CPU: {stats.cpu.lavalink_load:.2f}%")
542
543
# Monitor node health
544
if stats.memory.used > 1000: # Over 1GB memory usage
545
print("⚠️ High memory usage detected!")
546
547
if stats.cpu.lavalink_load > 80: # Over 80% CPU
548
print("⚠️ High CPU usage detected!")
549
```
550
551
### Advanced Error Recovery
552
553
```python
554
class RobustPlayer(wavelink.Player):
555
"""Enhanced player with automatic error recovery."""
556
557
def __init__(self, *args, **kwargs):
558
super().__init__(*args, **kwargs)
559
self.reconnect_attempts = 0
560
self.max_reconnect_attempts = 3
561
562
async def handle_disconnect(self):
563
"""Handle unexpected disconnections with retry logic."""
564
if self.reconnect_attempts < self.max_reconnect_attempts:
565
self.reconnect_attempts += 1
566
567
try:
568
# Wait before reconnecting
569
await asyncio.sleep(5 * self.reconnect_attempts)
570
571
# Attempt to reconnect
572
if self.channel:
573
await self.connect(self.channel)
574
print(f"Successfully reconnected (attempt {self.reconnect_attempts})")
575
576
# Resume playback if there was a current track
577
if self.current and not self.playing:
578
await self.play(self.current, start=self.position)
579
580
except Exception as e:
581
print(f"Reconnection attempt {self.reconnect_attempts} failed: {e}")
582
583
if self.reconnect_attempts >= self.max_reconnect_attempts:
584
print("Max reconnection attempts reached, giving up")
585
else:
586
print("Already exceeded max reconnection attempts")
587
588
async def safe_play(self, track, **kwargs):
589
"""Play with automatic retry on failure."""
590
for attempt in range(3):
591
try:
592
await self.play(track, **kwargs)
593
return
594
except Exception as e:
595
print(f"Play attempt {attempt + 1} failed: {e}")
596
if attempt < 2:
597
await asyncio.sleep(1)
598
else:
599
raise
600
601
# Use the robust player
602
@bot.command()
603
async def connect_robust(ctx):
604
"""Connect with enhanced error recovery."""
605
if ctx.voice_client:
606
return await ctx.send("Already connected!")
607
608
if not ctx.author.voice:
609
return await ctx.send("You're not in a voice channel!")
610
611
try:
612
player = await ctx.author.voice.channel.connect(cls=RobustPlayer)
613
await ctx.send("✅ Connected with enhanced error recovery!")
614
except Exception as e:
615
await ctx.send(f"❌ Failed to connect: {e}")
616
```
617
618
### Custom Event Logging
619
620
```python
621
import logging
622
from datetime import datetime
623
624
# Configure logging
625
logging.basicConfig(level=logging.INFO)
626
wavelink_logger = logging.getLogger('wavelink_events')
627
628
@bot.event
629
async def on_wavelink_track_start(payload):
630
"""Log track start events."""
631
player = payload.player
632
track = player.current
633
634
wavelink_logger.info(
635
f"Track started - Guild: {player.guild.id if player.guild else 'Unknown'}, "
636
f"Track: {track.title if track else 'Unknown'}, "
637
f"User Count: {len(player.channel.members) if player.channel else 0}"
638
)
639
640
@bot.event
641
async def on_wavelink_track_end(payload):
642
"""Log track end events with reason."""
643
player = payload.player
644
reason = payload.reason
645
646
wavelink_logger.info(
647
f"Track ended - Guild: {player.guild.id if player.guild else 'Unknown'}, "
648
f"Reason: {reason}, Queue: {player.queue.count} tracks"
649
)
650
651
@bot.event
652
async def on_wavelink_track_exception(payload):
653
"""Log track exceptions for debugging."""
654
player = payload.player
655
exception = payload.exception
656
657
wavelink_logger.error(
658
f"Track exception - Guild: {player.guild.id if player.guild else 'Unknown'}, "
659
f"Exception: {exception.get('message', 'Unknown')}, "
660
f"Severity: {exception.get('severity', 'Unknown')}"
661
)
662
663
# Command to view recent logs
664
@bot.command()
665
@commands.has_permissions(administrator=True)
666
async def view_logs(ctx, lines: int = 20):
667
"""View recent wavelink event logs."""
668
# In a production bot, you'd read from actual log files
669
await ctx.send(f"Check the console for the last {lines} wavelink events.")
670
```