1
# SPDX-License-Identifier: MIT
3
"""A basic example showing how to integrate audio from youtube-dl into voice chat."""
5
# NOTE: this example requires ffmpeg (https://ffmpeg.org/download.html) to be installed
6
# and available in your `%PATH%` or `$PATH`
8
# pyright: reportUnknownLambdaType=false
12
from typing import Any, Dict, Optional
15
import youtube_dl # type: ignore
16
from disnake.ext import commands
18
# Suppress noise about console usage from errors
19
youtube_dl.utils.bug_reports_message = lambda: ""
22
ytdl_format_options = {
23
"format": "bestaudio/best",
24
"outtmpl": "%(extractor)s-%(id)s-%(title)s.%(ext)s",
25
"restrictfilenames": True,
27
"nocheckcertificate": True,
28
"ignoreerrors": False,
32
"default_search": "auto",
33
"source_address": "0.0.0.0", # bind to ipv4 since ipv6 addresses cause issues sometimes
36
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
39
class YTDLSource(disnake.PCMVolumeTransformer):
40
def __init__(self, source: disnake.AudioSource, *, data: Dict[str, Any], volume: float = 0.5):
41
super().__init__(source, volume)
43
self.title = data.get("title")
47
cls, url, *, loop: Optional[asyncio.AbstractEventLoop] = None, stream: bool = False
49
loop = loop or asyncio.get_event_loop()
50
data: Any = await loop.run_in_executor(
51
None, lambda: ytdl.extract_info(url, download=not stream)
55
# take first item from a playlist
56
data = data["entries"][0]
58
filename = data["url"] if stream else ytdl.prepare_filename(data)
60
return cls(disnake.FFmpegPCMAudio(filename, options="-vn"), data=data)
63
class Music(commands.Cog):
64
def __init__(self, bot: commands.Bot):
68
async def join(self, ctx, *, channel: disnake.VoiceChannel):
69
"""Joins a voice channel"""
70
if ctx.voice_client is not None:
71
return await ctx.voice_client.move_to(channel)
73
await channel.connect()
76
async def play(self, ctx, *, query: str):
77
"""Plays a file from the local filesystem"""
78
await self.ensure_voice(ctx)
79
source = disnake.PCMVolumeTransformer(disnake.FFmpegPCMAudio(query))
80
ctx.voice_client.play(source, after=lambda e: print(f"Player error: {e}") if e else None)
82
await ctx.send(f"Now playing: {query}")
85
async def yt(self, ctx, *, url: str):
86
"""Plays from a url (almost anything youtube_dl supports)"""
87
await self._play_url(ctx, url=url, stream=False)
90
async def stream(self, ctx, *, url: str):
91
"""Streams from a url (same as yt, but doesn't predownload)"""
92
await self._play_url(ctx, url=url, stream=True)
94
async def _play_url(self, ctx, *, url: str, stream: bool):
95
await self.ensure_voice(ctx)
96
async with ctx.typing():
97
player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=stream)
98
ctx.voice_client.play(
99
player, after=lambda e: print(f"Player error: {e}") if e else None
102
await ctx.send(f"Now playing: {player.title}")
105
async def volume(self, ctx, volume: int):
106
"""Changes the player's volume"""
107
if ctx.voice_client is None:
108
return await ctx.send("Not connected to a voice channel.")
110
ctx.voice_client.source.volume = volume / 100
111
await ctx.send(f"Changed volume to {volume}%")
114
async def stop(self, ctx):
115
"""Stops and disconnects the bot from voice"""
116
await ctx.voice_client.disconnect()
118
async def ensure_voice(self, ctx):
119
if ctx.voice_client is None:
121
await ctx.author.voice.channel.connect()
123
await ctx.send("You are not connected to a voice channel.")
124
raise commands.CommandError("Author not connected to a voice channel.")
125
elif ctx.voice_client.is_playing():
126
ctx.voice_client.stop()
130
command_prefix=commands.when_mentioned,
131
description="Relatively simple music bot example",
137
print(f"Logged in as {bot.user} (ID: {bot.user.id})\n------")
140
bot.add_cog(Music(bot))
142
if __name__ == "__main__":
143
bot.run(os.getenv("BOT_TOKEN"))