0
# Nextcord Voice and Audio
1
2
Voice connection management, audio playbook, and voice state handling for Discord voice features with comprehensive audio processing capabilities.
3
4
## Voice Client
5
6
Core voice connection functionality for joining voice channels and managing audio playback.
7
8
### VoiceClient Class { .api }
9
10
```python
11
import nextcord
12
from nextcord import VoiceClient, AudioSource, PCMVolumeTransformer
13
from typing import Optional, Callable, Any
14
import asyncio
15
16
class VoiceClient:
17
"""Represents a Discord voice connection.
18
19
This is used to manage voice connections and audio playback in Discord voice channels.
20
21
Attributes
22
----------
23
token: str
24
The voice connection token.
25
guild: Guild
26
The guild this voice client is connected to.
27
channel: Optional[VoiceChannel]
28
The voice channel this client is connected to.
29
endpoint: str
30
The voice server endpoint.
31
endpoint_ip: str
32
The voice server IP address.
33
port: int
34
The voice server port.
35
ssrc: int
36
The synchronization source identifier.
37
secret_key: bytes
38
The secret key for voice encryption.
39
sequence: int
40
The current sequence number.
41
timestamp: int
42
The current timestamp.
43
mode: str
44
The voice encryption mode.
45
user: ClientUser
46
The bot user associated with this voice client.
47
"""
48
49
@property
50
def latency(self) -> float:
51
"""float: The latency of the voice connection in seconds."""
52
...
53
54
@property
55
def average_latency(self) -> float:
56
"""float: The average latency over the last 20 heartbeats."""
57
...
58
59
def is_connected(self) -> bool:
60
"""bool: Whether the voice client is connected to a voice channel."""
61
...
62
63
def is_playing(self) -> bool:
64
"""bool: Whether audio is currently being played."""
65
...
66
67
def is_paused(self) -> bool:
68
"""bool: Whether audio playback is currently paused."""
69
...
70
71
async def connect(
72
self,
73
*,
74
timeout: float = 60.0,
75
reconnect: bool = True,
76
self_deaf: bool = False,
77
self_mute: bool = False
78
) -> None:
79
"""Connect to the voice channel.
80
81
Parameters
82
----------
83
timeout: float
84
The timeout for connecting to voice.
85
reconnect: bool
86
Whether to reconnect on disconnection.
87
self_deaf: bool
88
Whether to deafen the bot upon connection.
89
self_mute: bool
90
Whether to mute the bot upon connection.
91
"""
92
...
93
94
async def disconnect(self, *, force: bool = False) -> None:
95
"""Disconnect from the voice channel.
96
97
Parameters
98
----------
99
force: bool
100
Whether to forcefully disconnect even if audio is playing.
101
"""
102
...
103
104
async def move_to(self, channel: nextcord.VoiceChannel) -> None:
105
"""Move the voice client to a different voice channel.
106
107
Parameters
108
----------
109
channel: nextcord.VoiceChannel
110
The voice channel to move to.
111
"""
112
...
113
114
def play(
115
self,
116
source: AudioSource,
117
*,
118
after: Optional[Callable[[Optional[Exception]], Any]] = None
119
) -> None:
120
"""Play an audio source.
121
122
Parameters
123
----------
124
source: AudioSource
125
The audio source to play.
126
after: Optional[Callable]
127
A callback function called after the audio finishes playing.
128
"""
129
...
130
131
def stop(self) -> None:
132
"""Stop the currently playing audio."""
133
...
134
135
def pause(self) -> None:
136
"""Pause the currently playing audio."""
137
...
138
139
def resume(self) -> None:
140
"""Resume the currently paused audio."""
141
...
142
143
@property
144
def source(self) -> Optional[AudioSource]:
145
"""Optional[AudioSource]: The currently playing audio source."""
146
...
147
148
# Basic voice connection example
149
@bot.command()
150
async def join(ctx):
151
"""Join the user's voice channel."""
152
if not ctx.author.voice:
153
await ctx.send("β You are not connected to a voice channel.")
154
return
155
156
channel = ctx.author.voice.channel
157
158
if ctx.voice_client is not None:
159
# Already connected, move to new channel
160
await ctx.voice_client.move_to(channel)
161
await ctx.send(f"π Moved to {channel.name}")
162
else:
163
# Connect to the channel
164
voice_client = await channel.connect()
165
await ctx.send(f"π Connected to {channel.name}")
166
167
@bot.command()
168
async def leave(ctx):
169
"""Leave the voice channel."""
170
if ctx.voice_client is None:
171
await ctx.send("β Not connected to a voice channel.")
172
return
173
174
await ctx.voice_client.disconnect()
175
await ctx.send("π Disconnected from voice channel.")
176
177
# Voice channel connection with error handling
178
async def connect_to_voice_channel(
179
channel: nextcord.VoiceChannel,
180
timeout: float = 10.0
181
) -> Optional[VoiceClient]:
182
"""Connect to a voice channel with proper error handling."""
183
try:
184
voice_client = await channel.connect(timeout=timeout)
185
print(f"Connected to {channel.name} in {channel.guild.name}")
186
return voice_client
187
188
except asyncio.TimeoutError:
189
print(f"Timeout connecting to {channel.name}")
190
return None
191
192
except nextcord.ClientException as e:
193
print(f"Already connected to voice: {e}")
194
return None
195
196
except nextcord.Forbidden:
197
print(f"No permission to connect to {channel.name}")
198
return None
199
200
except Exception as e:
201
print(f"Unexpected error connecting to voice: {e}")
202
return None
203
```
204
205
## Audio Sources
206
207
Audio source classes for playing various types of audio content.
208
209
### AudioSource Classes { .api }
210
211
```python
212
class AudioSource:
213
"""Base class for audio sources.
214
215
All audio sources must inherit from this class and implement the read method.
216
"""
217
218
def read(self) -> bytes:
219
"""Read audio data.
220
221
Returns
222
-------
223
bytes
224
20ms of audio data in PCM format, or empty bytes to signal end.
225
"""
226
...
227
228
def cleanup(self) -> None:
229
"""Clean up any resources used by the audio source."""
230
...
231
232
def is_opus(self) -> bool:
233
"""bool: Whether this source provides Opus-encoded audio."""
234
return False
235
236
class FFmpegPCMAudio(AudioSource):
237
"""An audio source that uses FFmpeg to convert audio to PCM.
238
239
This is the most common audio source for playing files or streams.
240
"""
241
242
def __init__(
243
self,
244
source: str,
245
*,
246
executable: str = 'ffmpeg',
247
pipe: bool = False,
248
stderr: Optional[Any] = None,
249
before_options: Optional[str] = None,
250
options: Optional[str] = None
251
):
252
"""Initialize FFmpeg PCM audio source.
253
254
Parameters
255
----------
256
source: str
257
The audio source (file path or URL).
258
executable: str
259
The FFmpeg executable path.
260
pipe: bool
261
Whether to pipe the audio through stdin.
262
stderr: Optional[Any]
263
Where to redirect stderr output.
264
before_options: Optional[str]
265
FFmpeg options to use before the input source.
266
options: Optional[str]
267
FFmpeg options to use after the input source.
268
"""
269
...
270
271
class FFmpegOpusAudio(AudioSource):
272
"""An audio source that uses FFmpeg to provide Opus-encoded audio.
273
274
This is more efficient than PCM audio as it doesn't require re-encoding.
275
"""
276
277
def __init__(
278
self,
279
source: str,
280
*,
281
bitrate: int = 128,
282
**kwargs
283
):
284
"""Initialize FFmpeg Opus audio source.
285
286
Parameters
287
----------
288
source: str
289
The audio source (file path or URL).
290
bitrate: int
291
The audio bitrate in kbps.
292
**kwargs
293
Additional arguments passed to FFmpegPCMAudio.
294
"""
295
...
296
297
def is_opus(self) -> bool:
298
"""bool: Always returns True for Opus sources."""
299
return True
300
301
class PCMVolumeTransformer(AudioSource):
302
"""A volume transformer for PCM audio sources.
303
304
This allows you to control the volume of audio playback.
305
"""
306
307
def __init__(self, original: AudioSource, volume: float = 0.5):
308
"""Initialize volume transformer.
309
310
Parameters
311
----------
312
original: AudioSource
313
The original audio source to transform.
314
volume: float
315
The volume level (0.0 to 1.0).
316
"""
317
...
318
319
@property
320
def volume(self) -> float:
321
"""float: The current volume level."""
322
...
323
324
@volume.setter
325
def volume(self, value: float) -> None:
326
"""Set the volume level.
327
328
Parameters
329
----------
330
value: float
331
The volume level (0.0 to 1.0).
332
"""
333
...
334
335
# Audio source examples
336
@bot.command()
337
async def play_file(ctx, *, filename: str):
338
"""Play an audio file."""
339
if not ctx.voice_client:
340
await ctx.send("β Not connected to a voice channel. Use `!join` first.")
341
return
342
343
try:
344
# Create audio source from file
345
source = nextcord.FFmpegPCMAudio(filename)
346
347
# Play the audio
348
ctx.voice_client.play(source, after=lambda e: print(f'Player error: {e}') if e else None)
349
350
await ctx.send(f"π΅ Now playing: {filename}")
351
352
except Exception as e:
353
await ctx.send(f"β Error playing file: {e}")
354
355
@bot.command()
356
async def play_url(ctx, *, url: str):
357
"""Play audio from a URL."""
358
if not ctx.voice_client:
359
await ctx.send("β Not connected to a voice channel.")
360
return
361
362
try:
363
# Use FFmpeg to stream from URL
364
# Common options for streaming
365
ffmpeg_options = {
366
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
367
'options': '-vn' # Disable video
368
}
369
370
source = nextcord.FFmpegPCMAudio(url, **ffmpeg_options)
371
372
ctx.voice_client.play(source)
373
await ctx.send(f"π΅ Now playing from URL")
374
375
except Exception as e:
376
await ctx.send(f"β Error playing from URL: {e}")
377
378
@bot.command()
379
async def play_with_volume(ctx, volume: float, *, filename: str):
380
"""Play an audio file with specified volume."""
381
if not ctx.voice_client:
382
await ctx.send("β Not connected to a voice channel.")
383
return
384
385
if not 0.0 <= volume <= 1.0:
386
await ctx.send("β Volume must be between 0.0 and 1.0")
387
return
388
389
try:
390
# Create audio source with volume control
391
original_source = nextcord.FFmpegPCMAudio(filename)
392
source = nextcord.PCMVolumeTransformer(original_source, volume=volume)
393
394
ctx.voice_client.play(source)
395
await ctx.send(f"π΅ Now playing {filename} at {volume:.0%} volume")
396
397
except Exception as e:
398
await ctx.send(f"β Error: {e}")
399
400
@bot.command()
401
async def volume(ctx, new_volume: float):
402
"""Change the volume of currently playing audio."""
403
if not ctx.voice_client or not ctx.voice_client.source:
404
await ctx.send("β Nothing is currently playing.")
405
return
406
407
if not isinstance(ctx.voice_client.source, nextcord.PCMVolumeTransformer):
408
await ctx.send("β Current audio source doesn't support volume control.")
409
return
410
411
if not 0.0 <= new_volume <= 1.0:
412
await ctx.send("β Volume must be between 0.0 and 1.0")
413
return
414
415
ctx.voice_client.source.volume = new_volume
416
await ctx.send(f"π Volume set to {new_volume:.0%}")
417
```
418
419
## Music Bot Implementation
420
421
Complete music bot implementation with queue management and playback controls.
422
423
### Music Bot System { .api }
424
425
```python
426
import asyncio
427
import youtube_dl
428
from collections import deque
429
from typing import Dict, List, Optional, Any
430
431
class Song:
432
"""Represents a song in the music queue."""
433
434
def __init__(
435
self,
436
title: str,
437
url: str,
438
duration: Optional[int] = None,
439
thumbnail: Optional[str] = None,
440
requester: Optional[nextcord.Member] = None
441
):
442
self.title = title
443
self.url = url
444
self.duration = duration
445
self.thumbnail = thumbnail
446
self.requester = requester
447
448
def __str__(self) -> str:
449
return self.title
450
451
@property
452
def duration_formatted(self) -> str:
453
"""Get formatted duration string."""
454
if not self.duration:
455
return "Unknown"
456
457
minutes, seconds = divmod(self.duration, 60)
458
hours, minutes = divmod(minutes, 60)
459
460
if hours:
461
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
462
else:
463
return f"{minutes:02d}:{seconds:02d}"
464
465
class MusicQueue:
466
"""Manages a music queue for a guild."""
467
468
def __init__(self):
469
self.queue = deque()
470
self.current_song = None
471
self.loop_mode = "off" # "off", "song", "queue"
472
self.shuffle = False
473
474
def add(self, song: Song) -> None:
475
"""Add a song to the queue."""
476
self.queue.append(song)
477
478
def next(self) -> Optional[Song]:
479
"""Get the next song from the queue."""
480
if self.loop_mode == "song" and self.current_song:
481
return self.current_song
482
483
if not self.queue:
484
if self.loop_mode == "queue" and self.current_song:
485
# Add current song back to queue for looping
486
self.queue.append(self.current_song)
487
else:
488
return None
489
490
if self.shuffle:
491
import random
492
song = random.choice(self.queue)
493
self.queue.remove(song)
494
else:
495
song = self.queue.popleft()
496
497
self.current_song = song
498
return song
499
500
def clear(self) -> int:
501
"""Clear the queue and return number of songs removed."""
502
count = len(self.queue)
503
self.queue.clear()
504
return count
505
506
def remove(self, index: int) -> Optional[Song]:
507
"""Remove a song by index."""
508
try:
509
return self.queue.popleft() if index == 0 else self.queue.remove(list(self.queue)[index])
510
except (IndexError, ValueError):
511
return None
512
513
def get_queue_list(self, limit: int = 10) -> List[Song]:
514
"""Get a list of songs in the queue."""
515
return list(self.queue)[:limit]
516
517
class YouTubeSource:
518
"""YouTube audio source with metadata extraction."""
519
520
ytdl_format_options = {
521
'format': 'bestaudio/best',
522
'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s',
523
'restrictfilenames': True,
524
'noplaylist': True,
525
'nocheckcertificate': True,
526
'ignoreerrors': False,
527
'logtostderr': False,
528
'quiet': True,
529
'no_warnings': True,
530
'default_search': 'auto',
531
'source_address': '0.0.0.0'
532
}
533
534
ffmpeg_options = {
535
'before_options': '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5',
536
'options': '-vn'
537
}
538
539
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
540
541
@classmethod
542
async def from_url(
543
cls,
544
url: str,
545
*,
546
loop: Optional[asyncio.AbstractEventLoop] = None,
547
stream: bool = False
548
) -> Dict[str, Any]:
549
"""Extract song information from YouTube URL."""
550
loop = loop or asyncio.get_event_loop()
551
552
# Run youtube-dl extraction in thread pool
553
data = await loop.run_in_executor(
554
None,
555
lambda: cls.ytdl.extract_info(url, download=not stream)
556
)
557
558
if 'entries' in data:
559
# Playlist - take first item
560
data = data['entries'][0]
561
562
filename = data['url'] if stream else cls.ytdl.prepare_filename(data)
563
564
return {
565
'title': data.get('title'),
566
'url': data.get('webpage_url'),
567
'stream_url': data.get('url'),
568
'duration': data.get('duration'),
569
'thumbnail': data.get('thumbnail'),
570
'filename': filename
571
}
572
573
@classmethod
574
def get_audio_source(cls, data: Dict[str, Any]) -> nextcord.AudioSource:
575
"""Get audio source from extracted data."""
576
return nextcord.FFmpegPCMAudio(data['filename'], **cls.ffmpeg_options)
577
578
class MusicBot:
579
"""Complete music bot implementation."""
580
581
def __init__(self, bot: commands.Bot):
582
self.bot = bot
583
self.guilds: Dict[int, MusicQueue] = {}
584
585
def get_queue(self, guild_id: int) -> MusicQueue:
586
"""Get or create a music queue for a guild."""
587
if guild_id not in self.guilds:
588
self.guilds[guild_id] = MusicQueue()
589
return self.guilds[guild_id]
590
591
async def play_next(self, ctx: commands.Context) -> None:
592
"""Play the next song in the queue."""
593
queue = self.get_queue(ctx.guild.id)
594
595
if not ctx.voice_client:
596
return
597
598
next_song = queue.next()
599
if not next_song:
600
# Queue is empty
601
embed = nextcord.Embed(
602
title="π΅ Queue Finished",
603
description="No more songs in the queue.",
604
color=nextcord.Color.blue()
605
)
606
await ctx.send(embed=embed)
607
return
608
609
try:
610
# Extract audio source
611
data = await YouTubeSource.from_url(next_song.url, stream=True)
612
source = YouTubeSource.get_audio_source(data)
613
614
# Play the song
615
ctx.voice_client.play(
616
source,
617
after=lambda e: self.bot.loop.create_task(self.play_next(ctx)) if not e else print(f'Player error: {e}')
618
)
619
620
# Send now playing message
621
embed = nextcord.Embed(
622
title="π΅ Now Playing",
623
description=f"**{next_song.title}**",
624
color=nextcord.Color.green()
625
)
626
627
if next_song.duration:
628
embed.add_field(name="Duration", value=next_song.duration_formatted, inline=True)
629
630
if next_song.requester:
631
embed.add_field(name="Requested by", value=next_song.requester.mention, inline=True)
632
633
if next_song.thumbnail:
634
embed.set_thumbnail(url=next_song.thumbnail)
635
636
await ctx.send(embed=embed)
637
638
except Exception as e:
639
await ctx.send(f"β Error playing {next_song.title}: {e}")
640
# Try to play next song
641
await self.play_next(ctx)
642
643
# Music bot commands
644
music_bot = MusicBot(bot)
645
646
@bot.command(aliases=['p'])
647
async def play(ctx, *, search: str):
648
"""Play a song from YouTube."""
649
if not ctx.author.voice:
650
await ctx.send("β You must be in a voice channel to use this command.")
651
return
652
653
# Connect to voice if not already connected
654
if not ctx.voice_client:
655
await ctx.author.voice.channel.connect()
656
elif ctx.voice_client.channel != ctx.author.voice.channel:
657
await ctx.voice_client.move_to(ctx.author.voice.channel)
658
659
# Show processing message
660
processing_msg = await ctx.send("π Searching...")
661
662
try:
663
# Extract song information
664
data = await YouTubeSource.from_url(search, loop=bot.loop, stream=True)
665
666
# Create song object
667
song = Song(
668
title=data['title'],
669
url=data['url'],
670
duration=data['duration'],
671
thumbnail=data['thumbnail'],
672
requester=ctx.author
673
)
674
675
# Add to queue
676
queue = music_bot.get_queue(ctx.guild.id)
677
queue.add(song)
678
679
# Edit processing message
680
if ctx.voice_client.is_playing():
681
embed = nextcord.Embed(
682
title="π Added to Queue",
683
description=f"**{song.title}**",
684
color=nextcord.Color.blue()
685
)
686
embed.add_field(name="Position in queue", value=len(queue.queue), inline=True)
687
embed.add_field(name="Duration", value=song.duration_formatted, inline=True)
688
embed.add_field(name="Requested by", value=ctx.author.mention, inline=True)
689
690
if song.thumbnail:
691
embed.set_thumbnail(url=song.thumbnail)
692
693
await processing_msg.edit(content=None, embed=embed)
694
else:
695
# Start playing immediately
696
await processing_msg.delete()
697
await music_bot.play_next(ctx)
698
699
except Exception as e:
700
await processing_msg.edit(content=f"β Error: {e}")
701
702
@bot.command()
703
async def skip(ctx):
704
"""Skip the current song."""
705
if not ctx.voice_client or not ctx.voice_client.is_playing():
706
await ctx.send("β Nothing is currently playing.")
707
return
708
709
queue = music_bot.get_queue(ctx.guild.id)
710
skipped_song = queue.current_song
711
712
ctx.voice_client.stop() # This will trigger play_next
713
714
embed = nextcord.Embed(
715
title="βοΈ Song Skipped",
716
description=f"Skipped **{skipped_song.title if skipped_song else 'Unknown'}**",
717
color=nextcord.Color.orange()
718
)
719
await ctx.send(embed=embed)
720
721
@bot.command()
722
async def queue(ctx, page: int = 1):
723
"""Show the music queue."""
724
music_queue = music_bot.get_queue(ctx.guild.id)
725
726
if not music_queue.current_song and not music_queue.queue:
727
await ctx.send("β The queue is empty.")
728
return
729
730
embed = nextcord.Embed(
731
title="π΅ Music Queue",
732
color=nextcord.Color.blue()
733
)
734
735
# Current song
736
if music_queue.current_song:
737
embed.add_field(
738
name="π΅ Now Playing",
739
value=f"**{music_queue.current_song.title}**\nRequested by {music_queue.current_song.requester.mention if music_queue.current_song.requester else 'Unknown'}",
740
inline=False
741
)
742
743
# Queue
744
queue_list = music_queue.get_queue_list(10)
745
if queue_list:
746
queue_text = []
747
for i, song in enumerate(queue_list, 1):
748
queue_text.append(f"{i}. **{song.title}** ({song.duration_formatted})")
749
750
embed.add_field(
751
name=f"π Up Next ({len(music_queue.queue)} songs)",
752
value="\n".join(queue_text),
753
inline=False
754
)
755
756
# Queue stats
757
if music_queue.queue:
758
total_duration = sum(song.duration for song in music_queue.queue if song.duration)
759
hours, remainder = divmod(total_duration, 3600)
760
minutes, _ = divmod(remainder, 60)
761
762
embed.set_footer(text=f"Total queue time: {hours}h {minutes}m | Loop: {music_queue.loop_mode}")
763
764
await ctx.send(embed=embed)
765
766
@bot.command()
767
async def pause(ctx):
768
"""Pause the current song."""
769
if not ctx.voice_client or not ctx.voice_client.is_playing():
770
await ctx.send("β Nothing is currently playing.")
771
return
772
773
ctx.voice_client.pause()
774
await ctx.send("βΈοΈ Paused the music.")
775
776
@bot.command()
777
async def resume(ctx):
778
"""Resume the paused song."""
779
if not ctx.voice_client or not ctx.voice_client.is_paused():
780
await ctx.send("β Nothing is currently paused.")
781
return
782
783
ctx.voice_client.resume()
784
await ctx.send("βΆοΈ Resumed the music.")
785
786
@bot.command()
787
async def stop(ctx):
788
"""Stop the music and clear the queue."""
789
if not ctx.voice_client:
790
await ctx.send("β Not connected to a voice channel.")
791
return
792
793
queue = music_bot.get_queue(ctx.guild.id)
794
cleared_count = queue.clear()
795
queue.current_song = None
796
797
ctx.voice_client.stop()
798
799
embed = nextcord.Embed(
800
title="βΉοΈ Music Stopped",
801
description=f"Cleared {cleared_count} songs from the queue.",
802
color=nextcord.Color.red()
803
)
804
await ctx.send(embed=embed)
805
806
@bot.command()
807
async def loop(ctx, mode: str = None):
808
"""Set loop mode (off, song, queue)."""
809
queue = music_bot.get_queue(ctx.guild.id)
810
811
if mode is None:
812
await ctx.send(f"Current loop mode: **{queue.loop_mode}**")
813
return
814
815
if mode.lower() not in ['off', 'song', 'queue']:
816
await ctx.send("β Invalid loop mode. Use: `off`, `song`, or `queue`")
817
return
818
819
queue.loop_mode = mode.lower()
820
821
loop_emojis = {
822
'off': 'β',
823
'song': 'π',
824
'queue': 'π'
825
}
826
827
await ctx.send(f"{loop_emojis[mode.lower()]} Loop mode set to: **{mode}**")
828
829
@bot.command()
830
async def shuffle(ctx):
831
"""Toggle shuffle mode."""
832
queue = music_bot.get_queue(ctx.guild.id)
833
queue.shuffle = not queue.shuffle
834
835
status = "enabled" if queue.shuffle else "disabled"
836
emoji = "π" if queue.shuffle else "β‘οΈ"
837
838
await ctx.send(f"{emoji} Shuffle {status}")
839
840
@bot.command()
841
async def nowplaying(ctx):
842
"""Show information about the currently playing song."""
843
queue = music_bot.get_queue(ctx.guild.id)
844
845
if not ctx.voice_client or not queue.current_song:
846
await ctx.send("β Nothing is currently playing.")
847
return
848
849
song = queue.current_song
850
851
embed = nextcord.Embed(
852
title="π΅ Now Playing",
853
description=f"**{song.title}**",
854
color=nextcord.Color.green(),
855
url=song.url
856
)
857
858
if song.duration:
859
embed.add_field(name="Duration", value=song.duration_formatted, inline=True)
860
861
if song.requester:
862
embed.add_field(name="Requested by", value=song.requester.mention, inline=True)
863
864
embed.add_field(name="Status", value="Playing βΆοΈ" if ctx.voice_client.is_playing() else "Paused βΈοΈ", inline=True)
865
866
if song.thumbnail:
867
embed.set_thumbnail(url=song.thumbnail)
868
869
# Queue info
870
if queue.queue:
871
embed.add_field(name="Next in queue", value=f"{len(queue.queue)} songs", inline=True)
872
873
embed.add_field(name="Loop", value=queue.loop_mode.title(), inline=True)
874
embed.add_field(name="Shuffle", value="On" if queue.shuffle else "Off", inline=True)
875
876
await ctx.send(embed=embed)
877
```
878
879
## Voice Events and State Management
880
881
Voice state tracking and event handling for advanced voice features.
882
883
### Voice State Events { .api }
884
885
```python
886
# Voice state event handlers
887
@bot.event
888
async def on_voice_state_update(
889
member: nextcord.Member,
890
before: nextcord.VoiceState,
891
after: nextcord.VoiceState
892
):
893
"""Handle voice state updates."""
894
895
# Member joined a voice channel
896
if before.channel is None and after.channel is not None:
897
print(f"{member} joined {after.channel.name}")
898
899
# Log join to a channel
900
log_channel = nextcord.utils.get(member.guild.channels, name="voice-logs")
901
if log_channel:
902
embed = nextcord.Embed(
903
title="π Voice Channel Joined",
904
description=f"{member.mention} joined {after.channel.mention}",
905
color=nextcord.Color.green(),
906
timestamp=datetime.now()
907
)
908
await log_channel.send(embed=embed)
909
910
# Member left a voice channel
911
elif before.channel is not None and after.channel is None:
912
print(f"{member} left {before.channel.name}")
913
914
# Check if bot should leave empty channel
915
if before.channel and len(before.channel.members) == 1:
916
# Only bot left in channel
917
bot_member = before.channel.guild.me
918
if bot_member in before.channel.members:
919
voice_client = nextcord.utils.get(bot.voice_clients, guild=member.guild)
920
if voice_client and voice_client.channel == before.channel:
921
await voice_client.disconnect()
922
print(f"Left {before.channel.name} - no other members")
923
924
# Log leave to a channel
925
log_channel = nextcord.utils.get(member.guild.channels, name="voice-logs")
926
if log_channel:
927
embed = nextcord.Embed(
928
title="π Voice Channel Left",
929
description=f"{member.mention} left {before.channel.mention}",
930
color=nextcord.Color.red(),
931
timestamp=datetime.now()
932
)
933
await log_channel.send(embed=embed)
934
935
# Member moved between channels
936
elif before.channel != after.channel:
937
print(f"{member} moved from {before.channel.name} to {after.channel.name}")
938
939
# Follow the user if they're the only one listening
940
voice_client = nextcord.utils.get(bot.voice_clients, guild=member.guild)
941
if voice_client and voice_client.channel == before.channel:
942
# Check if there are other non-bot members in the old channel
943
human_members = [m for m in before.channel.members if not m.bot]
944
if len(human_members) == 0 and member in after.channel.members:
945
await voice_client.move_to(after.channel)
946
print(f"Followed {member} to {after.channel.name}")
947
948
# Member mute/unmute status changed
949
if before.self_mute != after.self_mute:
950
status = "muted" if after.self_mute else "unmuted"
951
print(f"{member} {status} themselves")
952
953
# Member deaf/undeaf status changed
954
if before.self_deaf != after.self_deaf:
955
status = "deafened" if after.self_deaf else "undeafened"
956
print(f"{member} {status} themselves")
957
958
# Server mute/unmute
959
if before.mute != after.mute:
960
status = "server muted" if after.mute else "server unmuted"
961
print(f"{member} was {status}")
962
963
# Server deaf/undeaf
964
if before.deaf != after.deaf:
965
status = "server deafened" if after.deaf else "server undeafened"
966
print(f"{member} was {status}")
967
968
# Voice channel management commands
969
@bot.command()
970
async def voice_info(ctx, member: nextcord.Member = None):
971
"""Get voice information about a member."""
972
target = member or ctx.author
973
974
if not target.voice:
975
await ctx.send(f"β {target.display_name} is not in a voice channel.")
976
return
977
978
voice = target.voice
979
channel = voice.channel
980
981
embed = nextcord.Embed(
982
title=f"π Voice Info: {target.display_name}",
983
color=nextcord.Color.blue()
984
)
985
986
embed.add_field(name="Channel", value=channel.mention, inline=True)
987
embed.add_field(name="Channel Type", value=channel.type.name.title(), inline=True)
988
embed.add_field(name="Members", value=len(channel.members), inline=True)
989
990
if hasattr(channel, 'bitrate'):
991
embed.add_field(name="Bitrate", value=f"{channel.bitrate // 1000} kbps", inline=True)
992
993
if hasattr(channel, 'user_limit') and channel.user_limit:
994
embed.add_field(name="User Limit", value=channel.user_limit, inline=True)
995
996
# Voice state info
997
states = []
998
if voice.self_mute:
999
states.append("π Self Muted")
1000
if voice.self_deaf:
1001
states.append("π Self Deafened")
1002
if voice.mute:
1003
states.append("π Server Muted")
1004
if voice.deaf:
1005
states.append("π Server Deafened")
1006
if voice.self_stream:
1007
states.append("πΊ Streaming")
1008
if voice.self_video:
1009
states.append("πΉ Video On")
1010
1011
if states:
1012
embed.add_field(name="Status", value="\n".join(states), inline=False)
1013
1014
await ctx.send(embed=embed)
1015
1016
@bot.command()
1017
async def voice_stats(ctx):
1018
"""Show voice channel statistics for the server."""
1019
voice_channels = [c for c in ctx.guild.channels if isinstance(c, nextcord.VoiceChannel)]
1020
1021
if not voice_channels:
1022
await ctx.send("β This server has no voice channels.")
1023
return
1024
1025
embed = nextcord.Embed(
1026
title=f"π Voice Statistics - {ctx.guild.name}",
1027
color=nextcord.Color.blue()
1028
)
1029
1030
total_members = 0
1031
active_channels = 0
1032
1033
channel_info = []
1034
for channel in voice_channels:
1035
member_count = len(channel.members)
1036
total_members += member_count
1037
1038
if member_count > 0:
1039
active_channels += 1
1040
# Show members in channel
1041
member_names = [m.display_name for m in channel.members[:5]] # Show first 5
1042
member_text = ", ".join(member_names)
1043
if len(channel.members) > 5:
1044
member_text += f" and {len(channel.members) - 5} more"
1045
1046
channel_info.append(f"π **{channel.name}** ({member_count})\n{member_text}")
1047
1048
embed.add_field(
1049
name="Overview",
1050
value=f"**Total Channels:** {len(voice_channels)}\n"
1051
f"**Active Channels:** {active_channels}\n"
1052
f"**Total Members:** {total_members}",
1053
inline=False
1054
)
1055
1056
if channel_info:
1057
# Show first 5 active channels
1058
embed.add_field(
1059
name="Active Channels",
1060
value="\n\n".join(channel_info[:5]) +
1061
(f"\n\n... and {len(channel_info) - 5} more" if len(channel_info) > 5 else ""),
1062
inline=False
1063
)
1064
1065
await ctx.send(embed=embed)
1066
1067
# Voice channel moderation
1068
@bot.command()
1069
async def voice_kick(ctx, member: nextcord.Member, *, reason: str = None):
1070
"""Kick a member from their voice channel."""
1071
if not ctx.author.guild_permissions.move_members:
1072
await ctx.send("β You don't have permission to move members.")
1073
return
1074
1075
if not member.voice or not member.voice.channel:
1076
await ctx.send(f"β {member.mention} is not in a voice channel.")
1077
return
1078
1079
try:
1080
channel_name = member.voice.channel.name
1081
await member.move_to(None, reason=reason or f"Voice kicked by {ctx.author}")
1082
1083
embed = nextcord.Embed(
1084
title="π Voice Kick",
1085
description=f"{member.mention} has been disconnected from {channel_name}",
1086
color=nextcord.Color.orange()
1087
)
1088
1089
if reason:
1090
embed.add_field(name="Reason", value=reason, inline=False)
1091
1092
embed.set_footer(text=f"Action by {ctx.author}", icon_url=ctx.author.display_avatar.url)
1093
1094
await ctx.send(embed=embed)
1095
1096
except nextcord.Forbidden:
1097
await ctx.send("β I don't have permission to disconnect this member.")
1098
except nextcord.HTTPException as e:
1099
await ctx.send(f"β Failed to disconnect member: {e}")
1100
```
1101
1102
This comprehensive documentation covers all aspects of nextcord's voice and audio capabilities, providing developers with the tools needed to create sophisticated music bots and voice-enabled Discord applications.